@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
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ć if
y, 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ę.