Czemu używacie mockito/innych libek do mocków, zamiast fake objectów?

2

Wiem że mockito i generalnie całe to pojęcia mocków, stubów, spy'ów, etc. jest bardzo potężnie zakorzenione w rozwoju aplikacji, ale tak się zastanawiam...

Jak macie jakiś prosty interfejs:

interface Strategy {
  int doSomething(String string);
}

to czemu w kodzie widać takie cóś:

// given
Strategy strategy = mock(Strategy.class)
when(strategy.doSomething("xd")).thenReturn(2);

// given
bool value = objectUnderTest.perform(strategy, "XD");

zamiast zrobić fake'ową implementację

// when
bool value = objectUnderTest.perform(new FakeStrategy(2), "XD");
2

nie wiem skąd taki wniosek - u mnie używamy mocków np. w przypadku, gdy interfejs ma wiele metod a potrzebna jest tylko jedna albo dwie, ale pod warunkiem, że nie ma tam żadnej skomplikowanej logiki. Praktycznie cała reszta została zastąpiona fejkowymi klasami.

2

Mimo całej mojej niechęci do bibliotek do mockowania to pomijanie ich największego feature'a, jakim jest możliwość weryfikacji wywołań, jest nierzetelne. Oczywiście da się to zrobić własnymi klasami, ale to niepotrzebne rzeźbienie czegoś, co już zostało napisane.

4

Ja już dawno przestałem używać.

Większość rzeczy da się przetestować po efektach, a dla tych których się nie da np. sprawdzanie tytułu wysłanej wiadomości email przez FakeEmailSender zawsze można dopisać fakeEmailSender.getSendMessages() i dalej już to zwykłą asercją obrobić lub nawet fakeEmailSender.assertEmailWithTitleSent("foo") lub poprzez assertion object.

Pisałem o tym 3 lata temu: http://blog.marcinchwedczuk.pl/you-can-live-without-your-mocking-framework w sumie nie chce mi się już mordy otwierać jak nadal słyszę takie pytania...

4

Ja również jestem zwolennikiem używania fake'ów, ale to dla tego że sprawa jest głębsza niż tylko kweststia fake vs jakaś bilbioteka mockowania. Ja promuję testowanie w którym skupiamy się na zachowaniu (ang behaviour) procesu, a nie jego szczegółach implementacyjnych. Co za tym idzie, w większości przypadków testowanie tego czy jakaś metoda była wywołana (argument podany wyżej) jest znakiem że coś robimy nie tak, a nie czymś porządanym. W testach skupiam się na stosunkowo prostej zasadzie że dla konretnych danych wejściowych należy sprawdzać konkretne dane wyjściowe, a szczegóły implementacyjne takie jak to czy dana metoda została wywołana nie mają znaczenia. Więc bardziej tutaj chodzi o całą filozofię podejścia do tego co chcemy osiągnąć testami, niż technikalia.

Warto jednak zaznaczyć że ja ogólnie nie jestem zwolennikiem stosowania testów jednostkowych jako podstawowej metody testowania. W mojej pracy (jak i prywatnie) stosujemy podejście hybrydowe, gdzie większość testów pokrywa konkretny workflow, wliczając w to wysyłane w pamięci requesty HTTP, a cała reszta jest fake'owana (mocki stosujemy w wyjątkowych sytuacjach). Takie testy nazywamy behaviour tests ponieważ jak wcześniej wspomniałem testujemy zachowanie konkretnego procesu, nacisk kładąc na dane wejściowe i wyjściowe, a nie to co się dzieje pomiędzy. Ten rodzaj testów to u nas większość, testy jednostkowe są w mniejszości tam gdzie faktycznie ma sens testować konkretną, wyrwaną z kontekstu (to jeden z głównych powodów dla którego preferujemy testy behaviour, bo nie traci się kontekstu) jednostkę kodu- parsery, fabryki, funkcje mapujące itp.

8

pomijanie ich największego feature'a, jakim jest możliwość weryfikacji wywołań, jest nierzetelne.

Tylko, że w 99% (liczba z tyłka) weryfikowanie wywołań jest niedorzeczne.

Co kogo obczodzi czy funkcja dudaJasiuKarazula była wywołana? Jeśli testowana funkcja zwraca prawidłowy rezultat bez wywoływania jakiejś tam funkcji to jeszcze lepiej... Przy refaktoringach, bywa, że zmieniliśmy implementacje, wprowadziliśmy cache, nie zmieniając logik. Funkcja dudaJasiuKarazula jest wywoływana raz, a nie trzy razy jak w oryginalne... i dlatego mam failowac test?

Wyjątek - ten procent to jakieś serwisy/metody void, których nie da się tanio zamockować np. na poziomie tcpip. Coś w stylu "Mailer.sendMail" - czasem można odpuścić i nie stawiać tego smtp :-)
tylko po prostu sprawdzić jak serwis jest wołany. Przeważnie tak się robi (mockuje) jak są wołane jakies "enterprise" serwisy z porytym security.

Ogólnie, prawie nigdy nie używam Mockito sam z siebie. Jak pracuję z różnymi zespołami i rożnym kodem to mam o wiele wiecej zaufania do projektów opartych o ręcznie robione stuby itp. Może mają one mniejsze raportowane pokrycie (np. 70%) w stosunku do typowego projektu z mockito (90%), ale przynajmnie te testy czasem nawet wywalają się jak coś zrypie w logice, a nie tylko jak zmienie niesistotną kolejność wywoływania serwisów. Żeby było śmieszniej to testy oparte o mockito dość często w różnych projektach przepuszczały duże zwały (bo wynik też był mockowany...). (Miałem taki jeden projekt ekstremum, który zrobił ze mnie wściekle nienawidzącego mockito).

Btw. @TomRiddle w Twoim przykładzie to nawet zamiast FakeStrategy użyłbym ad hoc napisanej lambdy. Robię to dość często: - w końcu dobry interfejs ma dokładnie jedna metodę. A interfejs z jedną metodą jest równoważny funkcji.

4
TomRiddle napisał(a):

zamiast zrobić fake'ową implementację

Bo fejkową implementację trzeba napisać, a biblioteka jest darmowa.
Tylko ja raczej staram się nie tworzyć testy w taki sposób, żeby wszystko co potrzebne metodzie wpadało w jej argumentach wywołania, wtedy nie ma potrzeby robienia żadnego //given w ciele testu i tworzenia mocków, sutów, danych testowych, itp.

Aventus napisał(a):

W testach skupiam się na stosunkowo prostej zasadzie że dla konretnych danych wejściowych należy sprawdzać konkretne dane wyjściowe, a szczegóły implementacyjne takie jak to czy dana metoda została wywołana nie mają znaczenia.

Często nie ma danych wyjściowych, a wywołanie określonej metody jest jedynym efektem działania danej jednostki, a nie szczegółem implementacji.

7
Saalin napisał(a):

Mimo całej mojej niechęci do bibliotek do mockowania to pomijanie ich największego feature'a, jakim jest możliwość weryfikacji wywołań, jest nierzetelne.

Weryfikacja wywołań uwielbia się mścić przy robieniu jakichkolwiek zmian w kodzie testowanym przez weryfikację wywołań.

Raz, że kompletnie nie wiadomo co test miał faktycznie testować - wiesz tylko, że miał 3 razy zawołać zależność, ale nie masz pojęcia czemu :(

Dwa, że tak przetestowany kod jest bardzo odporny na jakiekolwiek refaktoringi - możesz przepisać kod na taki, który działa identycznie, ale wszystkie testy zfailują bo nie zgadzają się wywołania.

3

Mockowanie służy do testowania interakcji, tj. sprawdzania czy testowany kod przestrzega protokołu narzuconego przez inne moduły z nim współpracujące. Np. mockiem możesz sprawdzić, czy klasa, która jest cachem faktycznie uderza do zewnętrznego serwisu tylko raz, a za drugim razem bierze z cache'a. Mock daje co łatwy sposób weryfikacji liczby wywołań czy ich kolejności.

Natomiast zwykle testowanie (stuby, fake'i) skupia się na testowaniu logiki samej klasy, tzn czy efekty jej działania są poprawne. Tu byś sprawdził, czy cache zwraca poprawny obiekt, ale nie wykryłbys czy faktycznie wziął zapamiętany wcześniej czy uderzył uderzył jeszcze raz po to samo.

Oczywiście różnica jest subtelna i w sumie najczęściej w kodzie mam testowanie bez mocków.

0
somekind napisał(a):

Często nie ma danych wyjściowych, a wywołanie określonej metody jest jedynym efektem działania danej jednostki, a nie szczegółem implementacji.

Dla tego napisałem że w większości przypadków. Z tym że moim zdaniem to o czym piszesz to znaczna mniejszość, sytuacje takie jak wspomniał jarek czyli np. wysłanie maila. Ale wiadomo, każdy ma inne doświadczenia.

1
Aventus napisał(a):

Dla tego napisałem że w większości przypadków. Z tym że moim zdaniem to o czym piszesz to znaczna mniejszość, sytuacje takie jak wspomniał jarek czyli np. wysłanie maila. Ale wiadomo, każdy ma inne doświadczenia.

Być może mniejszość, ja po prostu wspomniałem o tym aspekcie, który wszyscy tutaj zdają się ignorować pisząc, że "mockowanie jest bez sensu".

2

Biblioteki do mockowania ułatwiają (w części przypadków) tworzenie fake objects. W przypadku z OP nie specjalnie widzę różnicę pomiędzy użyciem mocka, napisaniem fejkowej implementacji interface'u za po mocą klasy anonimowej, testowej, lambdy itd. Tak długo jak jest to czytelne oczywiście.

Druga sprawa - jaki jest sens pisania testu do interface'u?

2
piotrpo napisał(a):

Druga sprawa - jaki jest sens pisania testu do interface'u?

No oczywiście że to nie jest interfejsu :| Tylko test klasy, której interfejs jest parameterem. Mamy opcję zamockować interfejs, albo dodać fake'a.

2

A czemu miałbym pisać coś z palca co zwykle jest łatwiej ustawić?

Co do tego czy testować z użyciem mocka czy testować całość jako jednostkę i mocka nie używać a rzeczywistej implementacji - to osobny temat.
Tak samo czy jest sens testować service który wywołuje 3 inne i nic nie zwraca, a także nie ma w sobie żadnej logiki jednostkowo. Jeśli miałbym tam użyć mocków to mogę sobie ten test... no. Nic nie daje. Lepiej to faktycznie przetestować jako część testu integracyjnego (w sensie nie muszę zaczynać w tej klasie, może być po prostu używana w scenariuszu testu integracyjnego albo e2e).

Kolejną zaletą użycia mocka jest to, że nie potrzebuję robić 3 różnych faków do 3 różnych scenariuszy, ani skomplikowanej logiki - jeśli wywołam z A to daj mi 1, jak z B to daj mi 2.... a jak BIGJGJJG to 546456 ale jak z BIGGJJG to rzuć błędem. Praca z czymś takim to koszmar. Wolę mieć scenariusz, w nim gdzieś zapisane jak wywołuję X to dostaję 5 i nic mnie więcej nie obchodzi. Prosto i czytelnie i bez dużej liczby WTF/minutę.

Podkreślę jeszcze raz: trzeba się najpierw zastanowić czy mocka lub fake w ogóle używać,a jeśli tak to moim zdaniem już użyć biblioteki.

3

Odnoszę wrażenie, że zła reputacja bibliotek do mockowania wynika częściowo z faktu, że ktoś powiedział, że mockowanie jest złe. Z tego samego powodu hejtuje się masę innych technologii, jak JPA, Hibernate, Springa, ORM, kontenery IoC itd. W takiej sytuacji nawet jak się poda konkretny use case to jak krew w piach, bo przelatuje to koło uszu.

superdurszlak napisał(a):

Dwa, że tak przetestowany kod jest bardzo odporny na jakiekolwiek refaktoringi - możesz przepisać kod na taki, który działa identycznie, ale wszystkie testy zfailują bo nie zgadzają się wywołania.

Kod jest tak odporny na refactoring jak sobie to sami zgotujemy, był tu już przykład z Mailerem, ale no przecież nikt nie wywołuje zewnętrznych zależności w kodzie to po co to komu xD Czy interfejs EmailSender i sprawdzenie wywołania w teście to jest to co blokuje jakikolwiek refactoring?

3

Po pierwsze i najważniejsze - spytam tak: po co miałbym pisać za każdym razem sztuczną implementację obiektu zamiast korzystać z Mockito?
Tak na serio - nie widzę żadnego powodu - poza puryzmem językowym, którego specjalnie nie trawię - żeby specjalnie pisać fake objects.

Druga sprawa: dla interfejsów funkcyjnych sprawa jest prosta. Ale co jeśli interfejs ma pięć metod?

Np.

interface Taxi {
	int doSomethin1(String s);
	int doSomethin2(String s);
	int doSomethin3(String s);
	int doSomethin4(String s);
	int doSomethin5(String s);
}

Miałbym sobie stworzyć FakeTaxi, który w konstruktorze będzie przyjmował pięć elementów, z których wykorzystam jeden-dwa na test? Dzięki Mockito widząc test widzę od razu, co powinno być zwrócone.

A teraz hardmode - co jeśli chciałbym, żeby w jednym teście:

  • metoda doSomethin1 w zależności od podanego Stringa zwracała różną wartość
  • metoda doSomethin2 zawsze zwracała tę samą wartość
    A w drugim na odwrót? Przekazywać lambdy, przez co kod będzie wyglądał tak:
Taxi taxi = new FakeTaxi(
	e -> {
		if (e.equals("A") { return 1; }
		else if (e.equals("B") { return 2; }
		else if (e.equals("C") { return 3; }
		else { throw new RuntimeException(); }
	}, 
	e -> 1,
	e -> { throw new RuntimeException(); },
	e -> { throw new RuntimeException(); },
	e -> { throw new RuntimeException(); }
);

Taxi taxi2 = new FakeTaxi(
	e -> 1,
	e -> {
		if (e.equals("A") { return 1; }
		else if (e.equals("B") { return 2; }
		else if (e.equals("C") { return 3; }
		else { throw new RuntimeException(); }
	}, 
	e -> { throw new RuntimeException(); },
	e -> { throw new RuntimeException(); },
	e -> { throw new RuntimeException(); }
);

I to wszystko dlatego, że ktoś kiedyś na jakiejś konferencji usłyszał, że Mockito jest złe? xD

1

Po co testowac metody ktore nie daja efektu?
Ja zwykle testuje wynik dla podanych parametrow kompletnie ignorujac co sie i ile razy wywoluje.

1
somekind napisał(a):

Być może mniejszość, ja po prostu wspomniałem o tym aspekcie, który wszyscy tutaj zdają się ignorować pisząc, że "mockowanie jest bez sensu".

Ja tego aspektu nie zignorowałem, a wręcz się do niego odniosłem. Wspomniałeś o tym cytując moją wypowiedź więc założyłem że odnosisz się bezpośrednio do mnie, a nie ogólnie.

Tak czy inaczej mam nadzieję że jest jasne, że ja zgadzam się co do tego że mocowanie jest nadużywane, natomiast nie twierdzę aby było ogólnie bez sensu.

0

musicie pracowac z malym dobrym systemem, jezeli korzystanie z prawdziwych obiektow jest tak proste jak w pierwszym poscie. Co jak klasa ma N zaleznosci, a kazda z zaleznosci ma swoje N zaleznosc itd itd

1
Saalin napisał(a):

Kod jest tak odporny na refactoring jak sobie to sami zgotujemy, był tu już przykład z Mailerem, ale no przecież nikt nie wywołuje zewnętrznych zależności w kodzie to po co to komu xD Czy interfejs EmailSender i sprawdzenie wywołania w teście to jest to co blokuje jakikolwiek refactoring?

Jeden? Nie. Kilkadziesiąt? Sam sobie odpowiedz.

Dla mnie takie cudaki jak verify mogą być użyteczne w sytuacji, gdy dostajesz w prezencie kupę gruzu do utrzymania, testów nie ma i nie za bardzo da się napisać lepsze bez przepisywania wszystkiego. Skoro strach refaktorować, a bez refaktora nie napiszesz testów opartych wyłącznie o zwracane rezultaty / sprawdzanie efektów, no to trochę wyjścia nie ma. Natomiast pisanie nowego kodu i testowanie go w oparciu o verify a już szczególnie inorder albo argumentcaptor to strzelanie sobie w kolano.

Co do sprawdzania efektów - te też można sprawdzać nieco inaczej, niż patrząc ile razy Twój interfejs EmailSender został zawołany. Jeśli nic nie zwraca i nie może niczego sensownie zwracać, to możesz mieć implementację EmailSender która pisze sobie do pamięci, zamiast coś naprawdę wysyłać (w końcu to unit test i nie testujemy zewnętrznej zależności). I potem można ładnie podejrzeć, jakie efekty wykonał EmailSender bez sprawdzania, jak został zawołany i z jakimi argumentami.

2
superdurszlak napisał(a):

Co do sprawdzania efektów - te też można sprawdzać nieco inaczej, niż patrząc ile razy Twój interfejs EmailSender został zawołany. Jeśli nic nie zwraca i nie może niczego sensownie zwracać, to możesz mieć implementację EmailSender która pisze sobie do pamięci, zamiast coś naprawdę wysyłać (w końcu to unit test i nie testujemy zewnętrznej zależności). I potem można ładnie podejrzeć, jakie efekty wykonał EmailSender bez sprawdzania, jak został zawołany i z jakimi argumentami.

Ale to jest dokładnie to co zrobi verify bez rzeźbienia kodu weryfikującego samemu. Chyba, że masz inną ideę sprawdzania tej własnej, fake'owej implementacji mailera niż zapisanie tego jak został wywołany ("efektów").

4
Saalin napisał(a):

Ale to jest dokładnie to co zrobi verify bez rzeźbienia kodu weryfikującego samemu. Chyba, że masz inną ideę sprawdzania tej własnej, fake'owej implementacji mailera niż zapisanie tego jak został wywołany ("efektów").

Dla mnie różnica jest taka, że efekt mogę sobie trzymać w takiej postaci, jaka mi jest wygodna do testów i nie musi być co do joty tym, co w liście argumentów wywołań. Mogę sobie trzymać proste POJO i jak zamienię miejscami dwa argumenty w metodzie wywołania tego interfejsu, to nie muszę poprawiać 30 różnych verify.

2
superdurszlak napisał(a):

Dla mnie różnica jest taka, że efekt mogę sobie trzymać w takiej postaci, jaka mi jest wygodna do testów i nie musi być co do joty tym, co w liście argumentów wywołań. Mogę sobie trzymać proste POJO i jak zamienię miejscami dwa argumenty w metodzie wywołania tego interfejsu, to nie muszę poprawiać 30 różnych verify.

Musisz zmienić implementację tego InMemoryMailer. Moim zdaniem bardzo wątły argument. Jak masz klasę, która orkiestruje (?) operacje innych klas, to jej podstawowym zadaniem jest wywoływanie metod na zależnościach. Jakoś to verify musisz zaimplementować. Hipotetycznie można sobie rozważać, czy lepiej zrobić to mockiem, czy jakąś testową implementacją. Oba sposoby mogą doprowadzić do napisania przyzwoitego testu, który potwierdza działanie funkcjonalności, albo do pokrycia jakimś potworkiem, który testuje sam siebie. Dlatego uważam, że to o czym tu dyskutujemy jest kwestią drugorzędną.

1

Czemu używacie mockito/innych libek do mocków, zamiast fake objectów?

Nie używam, chyba ze do testowania czegoś bardzo dziwnego i inaczej sie nie da.

0
superdurszlak napisał(a):

Dla mnie różnica jest taka, że efekt mogę sobie trzymać w takiej postaci, jaka mi jest wygodna do testów i nie musi być co do joty tym, co w liście argumentów wywołań. Mogę sobie trzymać proste POJO i jak zamienię miejscami dwa argumenty w metodzie wywołania tego interfejsu, to nie muszę poprawiać 30 różnych verify.

Czyli po zmianie interfejsu verify poprawić musisz, ale swojego mocka już nie.
W sumie to nie jest złe podejście - jak się nie kompiluje, to żaden PO nie udowodni, że nie działa. Będzie z Ciebie dobry architekt.

5
wartek01 napisał(a):
Taxi taxi = new FakeTaxi(
	e -> {
		if (e.equals("A") { return 1; }
		else if (e.equals("B") { return 2; }
		else if (e.equals("C") { return 3; }
		else { throw new RuntimeException(); }
	}, 
	e -> 1,
	e -> { throw new RuntimeException(); },
	e -> { throw new RuntimeException(); },
	e -> { throw new RuntimeException(); }
);

Taxi taxi2 = new FakeTaxi(
	e -> 1,
	e -> {
		if (e.equals("A") { return 1; }
		else if (e.equals("B") { return 2; }
		else if (e.equals("C") { return 3; }
		else { throw new RuntimeException(); }
	}, 
	e -> { throw new RuntimeException(); },
	e -> { throw new RuntimeException(); },
	e -> { throw new RuntimeException(); }
);

I to wszystko dlatego, że ktoś kiedyś na jakiejś konferencji usłyszał, że Mockito jest złe? xD

To co pokazałeś to (o ile dobrze rozumiem) osobna klasa fake objectu dla każdego testu, co oczywiście nie ma dużego sensu, bo taka implementacja fake objectu jest mocno związana z testem i jego danymi, a nie z logiką produkcyjnej wersji klasy. Z tego powodu powyższy kod działa mniej więcej jak mock z mockito (i nie ma nad mockito większych zalet), a nie prawidłowa testowa implementacja. Porządna testowa implementacja powinna robić mniej więcej to samo co produkcyjna, ale być okrojona by ograniczyć I/O (zwłaszcza komunikację ze światem zewnętrznym). Dla przykładu testowe DAO może operować na kolekcjach, zamiast na pełnoprawnej bazie danych SQL. Do takiego DAO można wstrzyknąć dane (czyli te kolekcje), na których konkretny test ma operować.

Generalnie w testach mocki i tak muszą się zachowywać tak jak implementacje produkcyjne, bo o to właśnie chodzi (inaczej mielibyśmy testy do mocków, a nie testy do klas produkcyjnych). Mockowanie za pomocą mockito to ręczne nagrywanie i weryfikowanie prawidłowego zachowania się kodu. Jest to podatne na błędy w trakcie modyfikacji już istniejących klas. Jeśli zachowanie metody produkcyjnej się zmieni (np. zacznie rzucać exceptiony przy pewnych danych, modyfikować dane, pomijać pewne dane, etc) to wszystkie miejsca, gdzie było mockowanie przez mockito trzeba teraz przejrzeć i sprawdzić czy to ręcznie nagrane zachowanie jest zgodne z nowym zachowaniem klasy produkcyjnej (jeśli nie to trzeba poprawić). W przypadku testowej implementacji nie trzeba weryfikować testów w taki sposób - zamiast tego trzeba robić zmiany zachowania równolegle w implementacji produkcyjnej i testowej. Następnie odpalamy testy i te które trzeba poprawić od razu się sypią. W przypadku mocków z mockito, po zmianie samej implementacji produkcyjnej (bo testowej przecież nie ma), dużo więcej testów zostanie zielonych, bo zarówno testują kod według starego zachowania jak i mają nagrane stare zachowanie.

Co do wysyłania mejli:
Tutaj dobrze jest rozdzielić metody przygotowujące mejla od ich wysyłających. Jeżeli wysyłanie mejla jest warunkowe to też obliczanie warunku dobrze jest wydzielić do osobnej metody. W ten sposób można testować obie rzeczy (zarówno sprawdzanie warunku wysyłania mejla jak i jego treści) bez potrzeby mockowania. Obie metody zwracają wartości, które można sprawdzić w teście. Mockowanie przydaje się więc tylko dla testowania metod typu:

class MailService {
  ...
  void sendMail() {
    if (shouldSendEmail()) {
      mailer.sendMail(prepareEmail());
    }
  }
  ...
}

Jednak jest to na tyle prosta metoda, że nawet ręczny mock nie powinien tutaj bruździć. Można zrobić coś a'la:

@Test 
void testSendMail() {
  for (boolean shouldSend : new boolean[] { true, false }) {
    var sendCounter = new AtomicInteger();
    Mailer mailer = () => sendCounter.incrementAndGet();
    var mailService = new MailService() {
      @Override boolean shouldSendEmail() { return shouldSend; }
      @Override MailContent prepareEmail() { return null; }
    }
    mailService.sendEmail();
    assert(sendCounter.get() == shouldSend ? 1 : 0);
  }
}

Eleganckie nie jest, ale działa. Tego typu metod, gdzie dane wsiąkają i trudno sprawdzić co się z nimi potem dzieje, powinno być generalnie mało. Mało eleganckie testy do takich metod mogą być nawet motywacją, by minimalizować liczbę takich metod. Nie na każdego to działa, bo dużo ludzi lubi tworzyć trudno testowalne metody, a potem używać magicznych bibliotek do testowania trudno testowalnych metod.

0

@Wibowit:

Porządna testowa implementacja powinna robić mniej więcej to samo co produkcyjna, ale być okrojona by ograniczyć I/O (zwłaszcza komunikację ze światem zewnętrznym). Dla przykładu testowe DAO może operować na kolekcjach, zamiast na pełnoprawnej bazie danych SQL. Do takiego DAO można wstrzyknąć dane (czyli te kolekcje), na których konkretny test ma operować.

No spoko, to jeszcze raz zadaję pytanie: jak to zapisać, aby było czytelne?
Masz obiekt, który - dla uproszczenia - ma dwie metody coś zwracające.

public interface MyRest {
	int getValueA(int key);
	int getValueB(int key);
}

Teraz - napisz mi jakąś implementację testową, która obsłuży:
UC#1:

  • getValueA: dla kluczy: 1,2,3 zwróci odpowiednio 4,5,6
  • getValueB: dla kluczy: 1,2 zwróci odpowiednio 5,10

UC #2:

  • getValueA: dla klucza: 1 zwróci 4
  • getValueB: dla dowolnego klucza zwróci -1
    I zrób to tak, żeby to było spójne, żebym się nie naklepał przy tworzeniu tego kodu.

Tak już na koniec - problem, jaki tutaj widzę jest taki: ludzie z czasem odkryli, że nie wszystko należy mockować. Zgadzam się z tym jak najbardziej. Natomiast z jakiegoś powodu zamieniło to się w "nie należy nic mockować" - zapominając o prostej prawdzie, że fake object też jest mockiem a Mockito jest po prostu jednym ze sposobów na mockowanie obiektów.

Np. w twoim przypadku piszesz, że tak naprawdę należy pisać fake objecty pod I/O, żeby zaoszczędzić okład i testy śmigały szybko. Spoko, koncepcja słuszna, można się pokłócić. Natomiast docieramy do klasy DAO - wszyscy się zgadzają, że trzeba to zamockować. I teraz - jaka jest argumentacja do używania fake object zamiast Mockito?

2
wartek01 napisał(a):

@Wibowit:

Porządna testowa implementacja powinna robić mniej więcej to samo co produkcyjna, ale być okrojona by ograniczyć I/O (zwłaszcza komunikację ze światem zewnętrznym). Dla przykładu testowe DAO może operować na kolekcjach, zamiast na pełnoprawnej bazie danych SQL. Do takiego DAO można wstrzyknąć dane (czyli te kolekcje), na których konkretny test ma operować.

No spoko, to jeszcze raz zadaję pytanie: jak to zapisać, aby było czytelne?
Masz obiekt, który - dla uproszczenia - ma dwie metody coś zwracające.

public interface MyRest {
	int getValueA(int key);
	int getValueB(int key);
}

Teraz - napisz mi jakąś implementację testową, która obsłuży:
UC#1:

  • getValueA: dla kluczy: 1,2,3 zwróci odpowiednio 4,5,6
  • getValueB: dla kluczy: 1,2 zwróci odpowiednio 5,10

UC #2:

  • getValueA: dla klucza: 1 zwróci 4
  • getValueB: dla dowolnego klucza zwróci -1
    I zrób to tak, żeby to było spójne, żebym się nie naklepał przy tworzeniu tego kodu.

Podałeś przykład bez żadnych konkretnych danych, bez usecaseów, bez domeny biznesowej, podajesz tylko właściwie signatury i jakieś wartości. Oczywiście dla takiego przykładu ciężko znaleźć jakiś sensowny argument, bo jeśli faktycznie wymyślisz sobie dla kluczy: 1,2,3 zwróci odpowiednio 4,5,6 + dla kluczy: 1,2 zwróci odpowiednio 5,10, to to się wydaje dosyć dziwne. I na takich "gołych" danych nic sensownego nie wymyślisz. (Nawiasem mówiąc, gratuluje jeśli z mocka when(myRest.getValueA(1)).thenReturn(4) domyślisz się jaka jest odpowiedzialność klasy to gratuluje :D reszta z nas myślę jednak sie nie domyśli. Testy mają być opisowe, przekazywać intencje, czytając test powinieneś wiedzieć jaka była intencja piszącego test i co ten test na prawdę testuje).

Żeby tak na prawdę napisać dobrego fake'a pod Twoje dwa usecase'y, należałoby się zastanowić co takiego właściwie chciałbyś przetestować. Czym są 1,2,3 oraz czym są 4,5,6, jak również 1, 4 i -1. Czy to są id? Jeśli tak, to należałoby to nazwać IdProvider. Czy to są kolejności? Klucze do sortowania? Klucze mapy? Ilości? Bez kontekstu, ciężko coś wykminić.

Powiedz może czym są te liczby w Twoim przykładzie, i jak się mają do testowanej klasy, bo bez tego nic nie poradzimy. Bo wiesz, napisałeś tutaj bardzo konkretne, bardzo niskopoziomowe wartości, które na pierwszy rzut oka nie mają sensu, przynajmniej dla mnie. Myślę że nikt ich nie jest w stanie zrozumieć. Ja nie wiem czy to są id, klucze czy czym są te liczby. Nie wiem jak miałbym je zmienić/poprawić jeśli test zacznie fejkować. Nie wiem czy -1 ma jakieś specjalne znaczenie, czy jest tylko zwykłą liczbą.

I to jest właśnie cała idea problemu! Bo to nie z mockito mamy problem. Tylko właśnie z takimi danymi do testowania bez kontekstu. Jakbym zobaczył when(mock.getValueA(1,2,3)).thenReturn(4,5,6); to bym się złapał za głwoę i pomyślał, dokłądnie to samo co pomyślałem pisząc ten posty, czyli "co do c**** znaczą te liczby?" :o

screenshot-20210722160252.png

Daj nam szerszy kontekst.

PS: Może od razu dodam, że według mnie i myślę wszystkich tutaj, to jeśli to są Twoje kryteria, czyli dla 1,2,3 zwróć 4,5,6, to nie ważne czy napiszesz go z mockito czy z fake'ami, to efekt będzie ten sam: c**** test, nic nie wiadomo co on robi. Idęą fake'ów jest to, żę możesz zamknąć to sachowanie w klasie i nadać jej kontekst.

Np tak:

when(service.getValueB(1,2,3)).thenReturn(4,5,6); // chujowe
new Fake(asList(1,2,3), asList(4,5,6)); // chujowe, mimo że to fake

ale to już:

new IntegerDoublingService() {
  getValue(int param) {
    return 2 * param;
  }
} 

to już jest dobry fake. Nie dlatego że zwraca dane, ale dlatego żę przekazuje intencje.

Tak już na koniec - problem, jaki tutaj widzę jest taki: ludzie z czasem odkryli, że nie wszystko należy mockować. Zgadzam się z tym jak najbardziej. Natomiast z jakiegoś powodu zamieniło to się w "nie należy nic mockować" - zapominając o prostej prawdzie, że fake object też jest mockiem a Mockito jest po prostu jednym ze sposobów na mockowanie obiektów.

To z czym wszyscy się zgadzają, to to że nie należy przykazywać w testach zbyt wiele prawdziwych implementacji do testowanych obiektów.

Rozmowa polega na tym więc: "Jak dostarczyć tą nie-prawdziwą implementację", innymi słowy jak ją zrobić? Niektórzy (łącznie ze mną) preferują fake'owe implementacje, inni preferują korzystanie z mockito i innych podobnych bibliotek (czego nie rozumiem). Nie myl idei przekazywania mocków/fakeów/stubów, czego tam nie przekazujesz (bo idea jest słuszna - oddzielenie zależności od testowanej klasy) z jakimś jednym rozwiązaniem. Mickito nie ma wyłączności na decoupling. Wszyscy próbujemy to osiągnąć, niektórzy tylko w inny sposób.

Tutaj masz prawdziwy przykład:

interface UserNamesService {
  Set<String> usernames();
  Set<String> usernamesLimited(int limit);
}

interface Notifications {
  void notifyUserByRegistrationDate(Date date, Notification notification);
  void notifyUserByType(Type type, Notification notification);
  void notifyUserByPermission(Notification notification);
}

class MyService {
  void MyService(UserNamesService usernames, Notifications notifications) {}

  public void sendNotification() {}
}

Kod produkcyjny

new MySerivce(new DatabaseUsernames(), new EmailNotification()); // serwis pobierze nazwy z bazy i wyśle powiadomienia mailem

Teraz kod testowy, dwie wersje:

Wersja z fake'ami:

void test() {
  // given 
  var notifications = new MemoryNotifications(); // po prostu deklarujesz że powiadomienia mają być trzymane w pamięci
  var usernames = new ResourceUsernames("usernames.json"); // po prostu deklarujesz że chcesz username'y z pliku
  var objectUnderTest = new MyService(usernames, notifications);

  // when
  objectUnderTest.sendNotification();

  // then
  assertThat(notifications.getSentNotifications(), asList()); // dowolna asercja na wysłanych notyfikacjach
}

Wersja z mockami:

void test() {
  // given 
  var usernames = mock(UserNamesService.class);
  when(usernames.usernames()).thenReturn(loadFromFile("usernames.json")); // musisz wiedzieć że Twoja klasa użyje funkcji "usernames",

  var notifications = mock(Notifications.class);

  var objectUnderTest = new MyService(usernames , notifications);

  // when
  objectUnderTest.sendNotification();

  // then
  verify(notifications).sendNotificationByPermission(); // musisz wiedzieć ktorą metodą Twoja klasa wysłala powiadomienia
}

Jeśli spojrzysz na te kody, pierwsze co każdy Ci powie to to "musisz wiedzieć jakiej metody używa Twoja klasa", i że nie można metody dowolnie refaktorować, to prawda; ale używanie mocków z mockito ma też wiele dodatkowych wad, o któych mało kto mówi i pamięta.

Otóż, najważniejszy problem z mockito!!

Testy mają być czytelne. One nie są po to żeby coś zamockować, odpalić, i zrobić asercje. Testy mają wyrażać intencje, czytająć test masz wiedzieć po co on jest napisany; masz wiedzieć jakie zachowanie on testuje, co w klasie musiałoby się zmienić żeby test zaczął failować, co w klasie możę się zmienić żeby test przechodził - czyli jakie zachowanie jest wymuszane przez test. Musisz wiedzieć czy test testuje to samo zachowanie co inne testy lub nie. Musisz wiedzieć kiedy test można bezpiecznie usunąć jeśli wymagania się zmienią, musisz też wiedzieć kiedy go rozszerzyć, czyli dopisać inne testy na jego podstawie jeśli wymagania się zacieśnią. musisz wiedzieć czy i który test edytować jeśli wymagania zostaną update'owane,zmienione,usunięte dodane. Wszystko to musisz wyczytać z testu, i korzystanie z mockito skutecznie nam to uniemożliwia. Np kod when(usernames.usernames()).thenReturn(loadFromFile("usernames.json")). Czy czytając taki kod w mockito, jesteś w stanie stwierdzić czy:

  • Czy odpowiedzialnością metody usernames() jest dostarczenie nazw użytkowników, i loadFromFile("usernames.json") to jest parametr?

czy może

  • Czy odpowiedzialnością metody usernames() jest wczytywanie plików JSON z systemu plików, loadFromFile() to jest dummy implementacja a sam "usernames.json" jest parameterem?

Czytając sam test nie widać tego. Test nie wyraża intencji.

Korzystanie z mockito w testach ma tą wadę że logika testu i logika mockowania są pomieszane, przez co nie wiadomo co dokładnie jest testem a co nie. Po drugie mockito może jedynie dostarczać implementacje when() i weryfikować wywołania verify(), przez co bardzo ciężko stwierdzić co jest czym.

Kolejnym, bardzo dużym problemem z bibliotekami takimi jak mockito jest to, że one są zbudowane na metodach. Mockują metody i weryfikują metody. W podejściu testowym, to jest bardzo złe podejście. Dlatego że to, czy kod wykona dwie rzeczy w dwóch metodach; czy wszystko w jednej nie ma znaczenia. Testy powinny być identyczne. To co robi kod jest istotne i należy to testować, a nie jak kod to robi. Wielu programistów wpada w taką pułapke testowania bezpośredniej implementacji, klas, metod, pól, argumentów; zamiast zachowań/logiki. Masz testować ify, pętle, kolekcje, odniesienia, wyjątki i corner-casy. Nie funkcje i nie klasy. Jak wydzielę funkcje, użyję innej która robi to samo, test ma przechodzić nadal.

Pomyśl, tak na logikę. Piszesz test. Robisz verify(serivce).notifyUser(). Zastanów się tak na prawdę. Czy na prawdę chcesz, żeby ta metoda była wywowałana? Czy ona musi być wywołana? Czy może to co na prawdę chcesz, to to żeby notyfikacja była wysłana. Bo nie napisałeś testu pod notyfikacji, tylko pod metodę.

3
Saalin napisał(a):

Odnoszę wrażenie, że zła reputacja bibliotek do mockowania wynika częściowo z faktu, że ktoś powiedział, że mockowanie jest złe. Z tego samego powodu hejtuje się masę innych technologii, jak JPA, Hibernate, Springa, ORM, kontenery IoC itd. W takiej sytuacji nawet jak się poda konkretny use case to jak krew w piach, bo przelatuje to koło uszu.

Żeby było śmieszniej taki ktoś zapewne sam wcześniej nadużywał bibliotek do mockowania, co mu utrudniło życie, a potem wysnuł wniosek, że problemem jest narzędzie, a nie on sami.
Niestety tak to jest jak się uprawia tutorial and conference driven development i rzuca się na jakąś technologię jak szczerbaty na suchary, zupełnie bez zastanowienia.
Nadużywanie mockowania to np. mockowanie stable dependencies i powielanie w nich logiki biznesowej.

superdurszlak napisał(a):

Jeden? Nie. Kilkadziesiąt? Sam sobie odpowiedz.

Jak ktoś ma kilkadziesiąt interfejsów w zależnościach, to jego problem sięga daleko poza mockowanie czy testy.
Ale mimo tego, przy sensownie napisanych testach jednostkowych (nie klas) nie ma znaczenia, ile tam jest zależności pod spodem.

Dla mnie takie cudaki jak verify mogą być użyteczne w sytuacji, gdy dostajesz w prezencie kupę gruzu do utrzymania, testów nie ma i nie za bardzo da się napisać lepsze bez przepisywania wszystkiego. Skoro strach refaktorować, a bez refaktora nie napiszesz testów opartych wyłącznie o zwracane rezultaty / sprawdzanie efektów, no to trochę wyjścia nie ma. Natomiast pisanie nowego kodu i testowanie go w oparciu o verify a już szczególnie inorder albo argumentcaptor to strzelanie sobie w kolano.

Ta wypowiedź stoi w wewnętrznej sprzeczności sama ze sobą, bo verify służy do sprawdzania efektów.

Co do sprawdzania efektów - te też można sprawdzać nieco inaczej, niż patrząc ile razy Twój interfejs EmailSender został zawołany. Jeśli nic nie zwraca i nie może niczego sensownie zwracać, to możesz mieć implementację EmailSender która pisze sobie do pamięci, zamiast coś naprawdę wysyłać (w końcu to unit test i nie testujemy zewnętrznej zależności). I potem można ładnie podejrzeć, jakie efekty wykonał EmailSender bez sprawdzania, jak został zawołany i z jakimi argumentami.

No czyli dokładnie to, na co mock pozwala, tylko tworząc na ogół zbędną klasę.
Dla mnie to całe podejście wygląda jak bardzo silny syndrom NIH.

wartek01 napisał(a):

Tak już na koniec - problem, jaki tutaj widzę jest taki: ludzie z czasem odkryli, że nie wszystko należy mockować. Zgadzam się z tym jak najbardziej. Natomiast z jakiegoś powodu zamieniło to się w "nie należy nic mockować" - zapominając o prostej prawdzie, że fake object też jest mockiem a Mockito jest po prostu jednym ze sposobów na mockowanie obiektów.

Nie mockować, nie używać DI, nie używać ORMów. Było też nie używać baz danych, ale tutaj jakoś prawie każdy do nich nawiązuje. :D

TomRiddle napisał(a):

Idęą fake'ów jest to, żę możesz zamknąć to sachowanie w klasie i nadać jej kontekst.

Np tak:

when(service.getValueB(1,2,3)).thenReturn(4,5,6); // chujowe
new Fake(asList(1,2,3), asList(4,5,6)); // chujowe, mimo że to fake

ale to już:

new IntegerDoublingService() {
  getValue(int param) {
    return 2 * param;
  }
} 

to już jest dobry fake. Nie dlatego że zwraca dane, ale dlatego żę przekazuje intencje.

Skoro ten dobry fake zawiera logikę, to piszesz do niego testy? No bo jakby nie patrzeć wypadałoby.

To o czym piszesz, "nadawanie kontekstu" można osiągnąć również z mockami, tylko (moim zdaniem) taniej. Wystarczy tworzyć kawałki kodu zapewniające reużywalną customizację danych testowych.
Czy cały problem sprowadza się do tego, że przeciwnicy mocków kopiują ich konfigurację, zamiast wydzielić powtarzalne fragmenty?

Rozmowa polega na tym więc: "Jak dostarczyć tą nie-prawdziwą implementację", innymi słowy jak ją zrobić? Niektórzy (łącznie ze mną) preferują fake'owe implementacje, inni preferują korzystanie z mockito i innych podobnych bibliotek (czego nie rozumiem).

A ja nie rozumiem, co w ogólnym przypadku daje samodzielna implementacja istniejącego rozwiązania.

Mam SUT, który ma 5 zależności. Chcę wiedzieć, czy logika w SUT działa - czyli domyślnie potrzebuję, aby każdy z tych mocków zwracał niewybuchający wynik, który pozwoli na kontynuowanie flow. Biblioteka mi to daje właściwie za darmo.
Do tego potrzebuję sprawdzić, czy prawidłowo obsłużyłem errory pochodzące z każdej z tych zewnętrznych zależności. Czyli potrzebuję testów, w których ustawiam jednej z nich, żeby zwróciła błąd/wyjątek, a reszty nie ruszam. A jeśli efektem działania mojego kodu jest wywoływanie zewnętrznych zależności, to użyję verify aby to sprawdzić. To też mam za darmo.
No a poza tym, cytując klasyków, nie zaśmiecam namespace zbędną klasą. ;)

  // then
  verify(notifications).sendNotificationByPermission(); // musisz wiedzieć ktorą metodą Twoja klasa wysłala powiadomienia

Ale to chyba oczywiste? Jaki jest sens testu interakcji z zewnętrznym światem jeśli nie sprawdzimy, czy komunikujemy się w sposób wynikający z wymagań?

Jeśli spojrzysz na te kody, pierwsze co każdy Ci powie to to "musisz wiedzieć jakiej metody używa Twoja klasa", i że nie można metody dowolnie refaktorować, to prawda; ale używanie mocków z mockito ma też wiele dodatkowych wad, o któych mało kto mówi i pamięta.

Czego niby nie można refaktoryzować?!
Można sobie wszystko dowolnie refaktoryzować w swoim kodzie. Przecież sendNotificatonByPermission to nie jest Twój kod, to jest zależność. Nie tak miało być?

A jeśli to jest Twój kod, to idea mockowania czy libką czy własną klasą jest równie bezsensowna (na ogół).

Testy mają być czytelne. One nie są po to żeby coś zamockować, odpalić, i zrobić asercje. Testy mają wyrażać intencje, czytająć test masz wiedzieć po co on jest napisany; masz wiedzieć jakie zachowanie on testuje, co w klasie musiałoby się zmienić żeby test zaczął failować, co w klasie możę się zmienić żeby test przechodził - czyli jakie zachowanie jest wymuszane przez test. Musisz wiedzieć czy test testuje to samo zachowanie co inne testy lub nie. Musisz wiedzieć kiedy test można bezpiecznie usunąć jeśli wymagania się zmienią, musisz też wiedzieć kiedy go rozszerzyć, czyli dopisać inne testy na jego podstawie jeśli wymagania się zacieśnią. musisz wiedzieć czy i który test edytować jeśli wymagania zostaną update'owane,zmienione,usunięte dodane.

No super, ale Twoja wersja też żadnych intencji nie przekazuje. :P
Moim zdaniem intencje wyrażać należy przez nazwę przypadku testowego oraz poprawne nazwy elementów kodu odpowiedzialnych za dostarczenie danych do testu.

Korzystanie z mockito w testach ma tą wadę że logika testu i logika mockowania są pomieszane, przez co nie wiadomo co dokładnie jest testem a co nie. Po drugie mockito może jedynie dostarczać implementacje when() i weryfikować wywołania verify(), przez co bardzo ciężko stwierdzić co jest czym.

Nie da się wydzielać konfiguracji tego mockito do oddzielnych metod/klas i je ładnie nazywać? No to mi przykro, ale może w takim razie nie rozmawiajmy o Javie w dziale IO, skoro ekstrahowanie i enkapsulacja kodu są niemożliwe. :P

To co robi kod jest istotne i należy to testować, a nie jak kod to robi.

Przecież weryfikacja tego, czy wysłaliśmy maila, albo wywołaliśmy jakiekolwiek inne zewnętrzne API to jest właśnie testowanie tego, co kod robi.

Wielu programistów wpada w taką pułapke testowania bezpośredniej implementacji, klas, metod, pól, argumentów; zamiast zachowań/logiki. Masz testować ify, pętle, kolekcje, odniesienia, wyjątki i corner-casy. Nie funkcje i nie klasy. Jak wydzielę funkcje, użyję innej która robi to samo, test ma przechodzić nadal.

Pytanie zatem, w jakiej pułapce są ci programiści, którzy tak bardzo skupiają się na wynikach, że uznają iż zewnętrznych interakcji nie należy weryfikować.

4

Widziałem projekt z zamockowanymi repozytoriami JPA. To był dramat. Nic się nie dało zmienić, żeby testy nie zaczęły się sypać (chociaż równie dobrze można by usunąć te testy, bo nie sprawdzały działania kodu, tylko przeszkadzały).

Dla mnie mocki to trochę oszustwo. Im mniej tego cholerstwa, tym lepiej.

when(mockedUserRepository.save(any(User.class)).thenReturn(userToReturnFromRepository);

screenshot-20210722200838.png
#pdk

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