- fake repo ma metodki do łatwego wpychania danych do środka i wyciągania ich (nie pokazane w przykładzie poniżej) - skoro mamy kolekcje w środku to można na nich bezpośrednio operować zamiast na metodach repo
I jak często potrzebujesz repo pozwalającego na odczyt i zapis? Ja mam pewnie z 5 razy więcej "repozytoriów", które tylko czytam (czy to baza, czy to inny serwis itp), więc w testach potrzebuję wydmuszki, która dostaje dane przez konstruktor i potem zwraca je przy wywołaniu jakiejś metody do odczytu - nie widzę przewagi nad when.thenReturn z Mockito.
- fake repo testujemy dokładnie tymi samymi testami co real repo (pokazane w przykładzie poniżej) - to jest najważniejsza zaleta (w porównaniu do mockowania), bo dzięki temu infrastruktura testowa też jest gruntownie przetestowana i to praktycznie za darmo
Z jednej strony spoko, z drugiej strony trochę dziwne jest testowanie testów. Ale jak działa szybko i wymaga jednej linijki kodu, to można się w to bawić.
- w testach logiki biznesowej, (ewentualnie) kontrolerów, etc czyli niedotyczących bezpośrednio bazy używamy samego fake repo, bo jest szybkie (co jest lepsze niż mockowanie, bo fake repo jest przetestowane kompletem testów)
Jeżeli obawiasz się, że Twój mock zrobiony przy pomocy Mockito jest niepoprawny, to masz coś skopane niezależnie od Mockito.
- przy odrobinie pomysłowości fake repo prawdopodobnie mogłoby się przydać w szybkich i dobrze odizolowanych testach integracyjnych (tych testujących poprawność systemu, a nie wydajność). Nie sprawdziłem tego pomysłu w praktyce.
Nie bardzo widzę użyteczność tego, ani to prawdziwy load test, ani benchmark. Trudno wyrokować bez konkretnego przykładu użycia.
- fake repo można by traktować jako cache w przypadku, gdy całkowita ilość danych jest mała. Skoro
RealRepo
i FakeRepo
mają takie same metody to można by odpalać modyfikacje (np addUser
) na obu jednocześnie, a wczytywanie danych (np findUser
) tylko na FakeRepo
. Tego pomysłu też nie sprawdziłem w praktyce.
Raz, że tych danych musiałoby być naprawdę mało (powiedzmy gigabajty maks, żeby maszyna wytrzymała), a dwa, że to i tak nie zadziała w przypadku wielodostępu do bazy danych. Aczkolwiek niektóre frameworki tak keszują i działa, więc pewnie by zadziałało.
Można też wszystko (zarówno mockowanie jak i fake repo) olać i używać do każdego testu H2. Mam wrażenie, że moi koledzy do tego dążą. Mają chyba filozofię typu: skoro mamy kilkanaście mikroserwisów zamiast jednego monolitu to czas budowania pojedynczego mikroserwisu jest wielokrotnie krótszy niż budowanie monolitu, więc można zrobić powolne testy, a i tak mikroserwis będzie się budował krócej niż monolit z szybkimi testami.
To ma i tak tę wadę, że nie jest to ani test jednostkowy (= szybki), ani test integracyjny (= testujący prawdziwe komponenty).
U mnie często testy wyglądają w taki sposób (pseudokod):
Kopiuj
IRepository repository;
IService sut;
[BeforeEach]
void init(){
repository = mock();
sut = new Service(repository);
}
static class TestsForMethod1{
RepositoryResponse response;
[BeforeEach]
void init(){
response = genericResponseWithFullData();
}
[Test]
void test_czegos_prostego(){
// Tu modyfikujemy response, na przyklad jezeli któres z pól wynikowego JSON-a ma byc nullem, to robimy
response.property = null;
runTest();
assert();
}
[Test]
void test_czegos_grubego(){
// Albo jak potrzebujemy czegos grubszego, to nadpisujemy caly obiekt builderem czy innym DSL-em
reponse = specialResponse.builder().yada.build();
runTest();
assert();
}
void runTest(){
when(repository.Method()).thenReturn(response);
sut.doMagic();
}
}
static class TestsForRepositoryMethod2{
RepositoryResponse2 response; // Tu odpowiedz do innej metody
[BeforeEach]
void init(){
response = genericResponse2WithFullData();
}
[Test]
void test_czegos_tam(){
response.property = null;
runTest();
assert();
}
void runTest(){
when(repository.Method2()).thenReturn(response);
sut.doMagic2();
}
}
I mi się to sprawdza. Jak trzeba mi mądrego mocka, to mogę go łatwo wymienić w jednym miejscu, dodatkowo nie tworzę odpowiedzi w każdym teście, tylko wywołuję jakąś metodę tworzącą cały obiekt i potem nadpisuję jakieś fragmenty (albo używam DSL-a przy grubszych sprawach). Czasami zamiast robić assert w każdym teście, wyciągam oczekiwany wynik testy do pola, nadpisuję w każdym teście, a potem asercje wstawiam do runTest(). W efekcie każdy test jest krótki — najpierw mam nazwę metody mówiącą coś w stylu subtitlesInManyLanguages_firstLanguageSelected, arrange jest krótkie (bo to najczęściej kwestia jednej linijki), act i assert to też po jednej linii.
Przy okazji, przez użycie klas zagnieżdżonych nie muszę w każdym teście powtarzać nazwy metody testowanej.
Przy czym ja rzadko mam potrzebę testowania jakichś zaawansowanych interakcji z bazą czy coś. U mnie serwis strzela do innych serwisów, wyciąga z nich dane, młóci wszystko w pamięci, zwraca wynik dalej.
janek_sawicki