TDD w praktyce.

0

Jak wygląda TDD w praktyce?
1.Załóżmy prostą sytuację gdy piszemy testy jednostkowe do metody obliczającej czas. Mając ustawiony czas np na 8:00 i po dodaniu 30 minut, zegar powinien wskazywać 8:30. Prosty test, napisanie kilku takich dla żółtodzioba, nie znając implementacji metody, zajmie może parę minut.
Ale co jeśli, byśmy chcieli przetestować metodę w której będziemy potrzebować coś zmockować (np połączenie z Bazą danych i zwrócenie shardkodowanej Listy z jakimiś obiektami)? Albo, żeby nie łamać solid i utrzymać estetykę kodu trzeba będzie utworzyć dodatkową/e klasę. Teoretycznie można byłoby napisać testy i modyfikować je w chwili pisania głównej klasy, ale czy to nie będzie komplikowanie sobie życia? Mając napisanych 10 testów, do danej metody, przy pobieraniu rekordów z DB musielibyśmy w 10 miejscach jednocześnie coś mockować.

  1. Druga sytuacja. Załóżmy, że mamy mappera który mapuje jakąś klasę bazodanową na dto. Czy lepiej przetestować całą metodę mapującą, czy w metodzie mapującej wywoływać metody mapujące pola?
public User mappUserDtoToUser(){
  User user = new User;
  user.setUsername("username");
 ... // pozostałe pola
return user;
}
//czy:
public User mappUserDtoToUser(){
  User user = new User;
  setUsername(user,"username"); //do metody setUsername() zostałoby przeniesione user.setUsername("username");
 ... // pozostałe pola
return user;
}

Teoretycznie drugi test pozwalałby pisać dużo krótsze testy jednostkowe, ale więcej ich oraz zwiększyłby ilość funkcji w klasie mapującej. Albo w sytuacji gdybyśmy chcieli zapisać coś w bazie danych i rozbilibyśmy funkcję na openConnection(), persist(), closeConnection()?

1

łatwiej traktować IO (np baza danych) jako zewnętrzna zależność obiektu np przekazujesz obiektowi interface z typowymi operacjami bazodanowymi (CRUD czy cos). Wtedy podczas testów starczy, że zamiast prawdziwej implementacji podasz coś co działa w pamięci np. implementację opartą na hashMap<id,obiekt>.

Co do mapper. Warto testować mappera jednostkowo w ogóle? Zawsze możesz użyć jakiejś biblioteki w stylu ModelMapper

Btw. nie używaj setterów. Jeśli obiekt czegoś potrzebuje by istnieć przekazuj to w konstruktorze

0

Czy łatwiej? Nie powiedziałbym. Jeżeli chciałbyś potraktować DB jako IO i zamiast mockowania, stworzyłbyś klasę symulującą DB, to musiałbyś zaimplementować wszystkie pozostałe metody zawarte w Repozytorium (zwracając nulla w sytuacji gdy nie chciałbyś implementować metody, mogłoby się źle skończyć).
Dużo prościej i bezpieczniej (według mnie) jest zmockować dane metody i zwrócić jakieś obiekty przygotowane w metodzie oadnotowanej @Before.
Problem pojawia się w chwili, gdy używa się JPQL,

Testowanie mapperów ... ostatnio jeden z użytkowników, wspomniał o tworzeniu własnych mapperów (wraz z upraszczaniem typów zmiennych np. zwykły String zamiast Enuma, czy String reprezentujący datę w odpowiednim formacie) i powiem, że spodobał mi się ten pomysł. Testować - nie zaszkodzi, a może kiedyś pomoże.
Na dodatek, to był tylko przykład. Tak samo jak drugi przykład który podałem z zapisywaniem jakiegoś obiektu w bazie danych za pomocą własnoręcznie napisanych transakcji . Tutaj też według mnie lepiej wziąć, rozbić to na trzy funkcje (otwarcie połączenia, zapisanie obiektu, zamknięcie połączenia).

Edit: Jeszcze jedno pytanie odnośnie walidacji. Czy poprawnym (przy np walidowaniu nazwy Usera) jest rzucenie wyjątkiem np IllegalArgumentException()?

1

Ja nigdy nie rozumiałem czystego TDD i jakieś fanatyzmu polegającego na tym że zawsze testy muszą być pierwsze. Oczywiście można zrobić tak że stworzymy "kontrakt" przez metodę publiczną bez implementacji, później testy a później faktyczną implementację, a co do bazy danych i mockowania to tak jak pisał @Wibowit można utworzyć stuba in memory ;]
Generalnie tez nie warto wszystkiego na siłę testować jednostkowo moim zdaniem jeśli to wymaga zbyt dużo mocków, walnąc za to porządne integracyjne. Fanatyzm szkodzi ;]

3

Nie rozumiem. Może to kwestia dwóch mocnych piw, które wypiłem, ale kompletnie nie rozumiem co masz na myśli tutaj:

Albo, żeby nie łamać solid i utrzymać estetykę kodu trzeba będzie utworzyć dodatkową/e klasę.
Teoretycznie można byłoby napisać testy i modyfikować je w chwili
głównej klasy, ale czy to nie będzie komplikowanie sobie życia? Mając napisanych 10 testów, do danej metody, przy pobieraniu rekordów
z DB musielibyśmy w 10 miejscach jednocześnie coś mockować.

dlaczego napisanie testów to kompllikowanie sobie życia? O co chodzi z tą dodatkową klasą? W jaki sposób to wpływa na testy? Czemu 10 mocków to problem (abstrahując od tego czy mocki to hit czy kit - to przecież możesz stworzyć funkcję, która tworzy mocka i używać jej w 10 miejscach)?

0

Chodziło mi o to, że gdy mamy sytuację gdy piszemy testy jednostkowe dla danej metody. Później, "okazuje" się że w metodzie będziemy pobierać coś z bazy danych za pomocą repozytorium więc za pomocą mocka można podesłać jakąś listę obiektów. Mając napisane n testów, jeżeli metoda wymaga innego zestawu obiektów, trzeba w n miejscach zmockowac.
Z klasą to chodziło mi o to, jakbyśmy chcieli jakąś logikę do innej klasy wyodrębnić (np parsowanie Jsona) to wydaje mi się, że może znaleźć się sytuacja gdzie tutaj też trzeba będzie coś zmockować.

Pierwsze słyszę o stub in memory, co to jest?

1
Aisekai napisał(a):

Czy łatwiej? Nie powiedziałbym. Jeżeli chciałbyś potraktować DB jako IO i zamiast mockowania, stworzyłbyś klasę symulującą DB, to musiałbyś zaimplementować wszystkie pozostałe metody zawarte w Repozytorium (zwracając nulla w sytuacji gdy nie chciałbyś implementować metody, mogłoby się źle skończyć).
Dużo prościej i bezpieczniej (według mnie) jest zmockować dane metody i zwrócić jakieś obiekty przygotowane w metodzie oadnotowanej @Before.
Problem pojawia się w chwili, gdy używa się JPQL,

JPQL czy inne rzeczy sobie przetestujesz integracyjnie, natomiast testując zachowania modułów/klas testujesz jednostkowo. Nie oszukujmy się, zaimplementowanie CRUDowego repozytorium/DAO to bardzo prosta sprawa i nie zajmuje wiele czasu, a wielokrotnie będziesz z tego korzystać.

Testowanie mapperów ... ostatnio jeden z użytkowników, wspomniał o tworzeniu własnych mapperów (wraz z upraszczaniem typów zmiennych np. zwykły String zamiast Enuma, czy String reprezentujący datę w odpowiednim formacie) i powiem, że spodobał mi się ten pomysł. Testować - nie zaszkodzi, a może kiedyś pomoże.
Na dodatek, to był tylko przykład. Tak samo jak drugi przykład który podałem z zapisywaniem jakiegoś obiektu w bazie danych za pomocą własnoręcznie napisanych transakcji . Tutaj też według mnie lepiej wziąć, rozbić to na trzy funkcje (otwarcie połączenia, zapisanie obiektu, zamknięcie połączenia).

Edit: Jeszcze jedno pytanie odnośnie walidacji. Czy poprawnym (przy np walidowaniu nazwy Usera) jest rzucenie wyjątkiem np IllegalArgumentException()?

Co do edita - to zależy. Od wielu czynników, np. jak masz mapowane wyjątki, tzn jak trafiają do usera czy tam generalnie poza moduł. IMHO można.
Generalnie po co testować mappery? Masz jakiś obiekt, następnie wołasz coś co zapisuje ci ten obiekt gdzie chcesz i potem sprawdzasz co się zapisało. Zakładałbym, że jeżeli np. korzystasz bezpośrednio z jakiegoś drivera do bazy danych czy tam spring data albo czegoś innego to ktoś inny to przetestował żeby pola nie zginęły po drodze, czyli jeżeli test się nie powiedzie to albo zepsuleś mapper albo nie potrafisz korzystać z narzędzi (np. ww drivera)

Albo w sytuacji gdybyśmy chcieli zapisać coś w bazie danych i rozbilibyśmy funkcję na openConnection(), persist(), closeConnection()?

Jak chcesz coś zapisać to wystaw sobie interfejs z jakąs zgrabną metodą save. W testach jednostkowych dotykanie I/O i testowanie otwierania połączenia i takich detali jest moim zdaniem nie na miejscu. W testach jednostkowych chcesz przetestować zachowanie np. ktoś chce zarejestrować użytkownika. Efekt jest taki, że użytkownik zostaje zarejestrowany, gdzieś tam się zapisały jego dane. To w jaki sposób jest to robione pod spodem, czy to JPQL czy springowe repository, czy do postrgesa czy do mongo nie ma znaczenia. Nie ma co się betonować z technologią, nie o to chodzi

Pierwsze słyszę o stub in memory, co to jest?

public interface TeamRepository {

    List<Team> findAll();

    Team save(Team team);
}

class InMemoryTeamRepository implements TeamRepository {

    private final Map<String, Team> storage = new HashMap<>();

    @Override
    public List<Team> findAll() {
        return new ArrayList<>(storage.values());
    }

    @Override
    public Team save(Team team) {
        storage.put(team.toExistingTeam().getName(), team);
        return team;
    }
}

wtedy nie mockujesz danych testująć, tylko np masz test, gdzie testujesz powiedzmy zmiane adresu uzytkownika. To na poczatku takiego testu trzeba zalozyc, ze taki uzytkownik w ogole istnieje, wiec go zapisujesz, czyli normalnie po bozemu wkladasz do repo albo najlepiej przez cały moduł (jakas tam fasadę czy coś innego)

0

Aaaa, czyli stub in memory jest po prostu klasą która:

  1. Implementuje dany interfejs pełniący rolę repozytorium.
  2. Pełni rowniez rolę bazy damych

Mapper czy połączenie z bazą danych było tylko przykładem, w którym chodziło mi o to czy lepiej daną metodę rozbić na kilka mniejszych i testować te mniejsze z założeniem, że jeżeli wszystkie metody przejdą testy to metoda główna też je przejdzie (więc test metody głównej jest zbędny) czy testować pewną funkcjonalność (jak dodanie oferty na giełdzie z uwzględnieniem czy kupującego na to stać, oraz automatycznym zakupem), ale na to pytanie już odpowiedź uzyskałem więc dzięki :)

3

Jeszcze co do TDD. Według mnie nie ma sensu testować każdej klasy tylko dla samego jej testowania. Lepsze według mnie jest coś w stylu BTT na poziomie modułów. Zamiast jednostkowych u siebie testuje moduły (jako moduł mam na myśli kilka klas w jednym package, z jedną publiczną klasą jako API dla reszty systemu). Mniej więcej przed pisaniem modułu wiem jakie zachowanie ma mieć (bo w końcu testuje się zachowania, a nie kod, podobno...). Podczas pisania pierwszych testów, tworzę sobie obiekt fasady (czyli owe API) z pustymi metodami. Dzięki temu mam kompilujące, wywalające się testy i później już tworzę faktyczne obiekty z logiką, które są używane przez tą fasadę

0

Ja się zawsze staram rozbijać klasy na mniejsze, każda powinna mieć jedną odpowiedzialność (SOLID - wiemy).
Następnie piszę specyfikację - test jednostkowy, który określa, co dana metoda, powinna robić dla danego warunku.
Ważne jest, aby przechodzić szybko czerwony -> zielony -> refaktor.
Robię to od dwóch miesięcy i u mnie się sprawdza - mam "żywą" dokumentację tego jak działa kod.
Wymusza to na mnie patrzenie na więcej przypadków i nieco szerszy kontekst, więcej pytam

W tym momencie, poza tym, że mój kod jest SOLIDny, to jest jeszcze testowalny.
Oszczędza mi to też masę czasu, ponieważ, gdybym musiał za każdym razem stawiać platformę, na której pracuję, to sprawdzanie mojego kodu trwało by bardzo długo (każdy rebuild to około 5 minut, plus start platformy następne 4, a doklikaj się jeszcze do wywołania swoje funkcji - a to np. dwie linijki kodu ). Jak kończę zadanie, to testuje wszystko maualnie (lub piszę testy intgracyjne, gdzie się da) i oddaje do code review, a potem testów.

Generalnie uważam że TDD jest przydatne, efekty widać dopiero gdy cały zespół zaczyna używać.
Gdzieś po około 6-12 miesięcy powinien być zauważalny spadek zwrotek błędów.

2

@danek "jednostkowy" nie oznacza klasowy :) równie dobrze twoją jednostką może być moduł

1

Wartościowy film confitury związany z tematem: Linkuje do miejsca gdzie pan Jakub mówi bardzo zbieżne informacje do @danek. Warto zapoznać się z całością.

0

Chodziło mi o to, że gdy mamy sytuację gdy piszemy testy jednostkowe dla danej metody.
Później, "okazuje" się że w metodzie będziemy pobierać coś z bazy danych za pomocą repozytorium więc za pomocą mocka można podesłać jakąś listę obiektów.

Dalej nie rozumiem. Po pierwsze wydaje mi się, że architektura może być zła, skoro może się "okazać", że pewne metody jakiejś tam klasy X nagle zaczną pobierać coś z bazy danych (co na przykład?). Nie lepiej żeby jedna klasa A żądała od klasy B (w twoim przykładzie "repozytorium") pewnych danych i podawała klasie X same dane do obróbki?

Od strony testów pewnie efekt byłby podobny, może trochę lepszy o tyle, że musiałbyś mockować same dane zamiast całej klasy "repozytorium", ale jakoś architektonicznie wydaje mi się to lepsze.

Chociaż wtedy niby musiałby przetestować dodatkowo klasę A, która kontaktuje się z klasami B oraz X. Tylko, że moim zdaniem to i tak trzeba byłoby bardziej "integracyjnie" przetestować ("integracyjnie" w sensie, żeby zintegrować rózne prawdziwe moduły w aplikacji, czyli żeby nie mockować poszczególnych modułów, tylko żeby odpalić aplikację albo jakiś jej podsystem w całej okazałości. Ale same dane mogłyby być zamockowane z fejkowej bazy).

Tylko, że to i tak można by nazwać unit testem, po prostu unitem musiałoby być coś innego niż "metoda" czy "klasa" (może np. unit - "zachowanie"?). Testy jednostkowe są tak elastycznym pojęciem, że dużo można pod to podciągnąć.

Z klasą to chodziło mi o to, jakbyśmy chcieli jakąś logikę do innej klasy wyodrębnić (np parsowanie Jsona)

Nie ma w Javie bibliotek do parsowania JSONa? o.O

Mając napisane n testów, jeżeli metoda wymaga innego zestawu obiektów, trzeba w n miejscach zmockowac.

A nie możesz zrobić jakiegoś osobnego pliku na utilsy do testów i tam wrzucić wszelkie mocki, i potem importować te mocki do pliku, który definiuje testy?

0

Dalej nie rozumiem. Po pierwsze wydaje mi się, że architektura może być zła, skoro może się "okazać", że pewne metody jakiejś tam klasy X nagle zaczną pobierać coś z bazy danych (co na przykład?). Nie lepiej żeby jedna klasa A żądała od klasy B (w twoim przykładzie "repozytorium") pewnych danych i podawała klasie X same dane do obróbki?

Tak, właśnie o to mi chodziło, że np serwis zażąda od repozytorium, żeby ten pobrał jakieś dane z bazy danych. Wtedy można zmockować np. metodę repository.findAll() żeby zwracała jakąś listę obiektów, albo zrobić stub in memory.

Nie ma w Javie bibliotek do parsowania JSONa? o.O

Jest i właśnie chodziło mi o to, żeby np serwis zażądał od jakiegoś JsonMappera (ObjectMapper), żeby ten zmapował Jsona. Teoretycznie, żeby ułatwić sobie testy można byłoby zmockować to tak, żeby przy wywołaniu metody mapującej, pominąć mapowanie i od razu do jakiejś zmiennej przypisać obiekt.

A nie możesz zrobić jakiegoś osobnego pliku na utilsy do testów i tam wrzucić wszelkie mocki, i potem importować te mocki do pliku, który definiuje testy?

Prawdę mówiąc, od niedawna zacząłem bawić się w testowanie i o czymś takim pierwsze słyszę, a może pomoże to w pisaniu testów. Nie wiem do końca jeszcze co testować, więc w ramach ćwiczeń staram się testować większość kodu jaki napisałem.

Jeszcze jedno pytanie. Załóżmy taką sytuację:

interface C{
 String method();
}

class Foo implements C{
   public String method(){
    }
}

class Boo implements C{
    public String method(){
    }
}

To jakby wyglądały testy?


class FooTest{
    Foo foo; // czy C foo;?
    //kod testujący
}

class BooTest{
    Boo boo; //czy C boo;?
    // kod testujący
}

Tzn tworząc obiekt klasy Boo lub Foo, posługujemy się interfejsem czy właściwą implementacją? Według mnie interfejsem.

1

Nigdy nie testujesz interfejsu tylko konkretne implementacje. Jednak zdarza się, że używasz interfejsu w kodzie i wtedy w testach też używasz interfejsu żeby pokazać jakie zachowanie ma realizować, np. zapisać coś do bazy lub też wykonać płatność.Czyli np. używasz repozytorium jako interfejsu w kodzie to interesuje cie zachowanie głównie takie, zeby zapisac obiekt lub go wyciągnąć. gdy bedziesz testował implementacje to tam cie interesuje czy np. driver zamyka połączenie po każdej operacji, czy je utrzymuje, jakie wspiera operacje danego silnika etc

1

polecam obejrzeć filmiki od Robert C. Martin (AKA Uncle Bob).
Szczególnie jeśli masz dostęp do Sfari

Za free też są dostępne https://www.google.com/search?&q=tdd+uncle+bob ale nie są tak dobre.
To ogląłem i jest ok

1 użytkowników online, w tym zalogowanych: 0, gości: 1