Ciężkie testy - potrzeba zamockowania wewnętrznych, prywatnych obiektów

0

Mam takie zależności w projekcie (mini PaaS)

AppController 🡒 AppFacade 🡒 AppManager
AppManager 🡒 {GitCloner, DockerImageManager, DockerContainerManager}
AppFacade 🡒 AppRepository 🡒 Baza

Scenariusz (w uproszczeniu): użytkownik wysyła POST zawierający link do repozytorium z kodem aplikacji, a mój system ją testuje i wdraża (Docker).

  1. POST
  2. AppController: sprawdzenie poprawności DTO i przekazanie go do fasady
  3. AppFacade tworzy instancję AppManager i wywołuje odpowiednie metody (AppManager - wysokopoziomowa klasa do wdrażania aplikacji i zarządzania nimi)
  4. AppManager używa GitClonera do sklonowania repo. GitCloner zwraca ścieżkę do sklonowanego na dysk kodu
  5. AppManager używa DockerImageManagera oraz DockerContainerManagera do zbudowania obrazu ze sklonowanego kodu i uruchomienia kontenera
  6. Wracamy do AppFacade. AppFacade wyciąga sobie potrzebne informacje z instancji AppManager.
    Uwaga: wyciągane informacje pochodzą też z "żywego" systemu, gdzie konieczne jest zapytanie Dockera (za pośrednictwem DockerContainerManagera) o informacje na temat działającego kontenera.
  7. AppFacade zapisuje dane do bazy, korzystając z AppRepository
  8. AppFacade tworzy DTO, które następnie zwraca do kontrolera
  9. Użytkownik otrzymuje odpowiedź

Jak to testować? Chodzi o to, żeby podczas testów fasady/kontrolera zamockować punkty 4 i 5 (częściowo też pkt 6 - wyciąganie informacji o kontenerze), które zabierają bardzo dużo czasu.
Innymi słowy: GitCloner, DockerImageManager i DockerContainerManager w testach fasady/kontrolera muszą odejść. Są już przetestowane jednostkowo.

Problem: nie mam bezpośredniego dostępu do tych klas. Ich obiekty są tworzone i używane wewnątrz AppManagera i na zewnątrz nikt o nich nie wie.

Mój plan: zrobić mocki GitClonera, DockerImageManagera i DockerContainerManagera (PowerMock da radę?).
Ale...
..słyszałem dużo złego o używaniu PowerMock (typu: jeżeli potrzebny jest PowerMock to znaczy, że z kodem jest coś nie tak). Więc może rzeczywiście mam kupę w kodzie? :/

Od razu mówię, że nie robiłem jeszcze mocków i testów dla "rozbudowanego" systemu.

Jeżeli potrzebne jest spojrzenie na kod: https://github.com/Potat0x/PotaPaaS-Service/tree/master/src/main/java/pl/potat0x/potapaas/potapaasservice
Klasy, które chcę zamockować siedzą w pakiecie core.

Będę wdzięczny za wskazówki :)

2

Zrobić przykładowe repozytorium z kodem, odpalić cały soft bez mockow i przetestować czy wyprodukowany obraz dziala tworzac efektywnie e2e/integration testy zamiast unitów.

90% twojego kodu to operacje IO i 3rd party toole. Testowanie takiego glue-code unitami mija się z celem.

2

Jedno pytanie: po co?
Stawiasz sobie bazę in memory, stawiasz prosty httpserver który udaje tego twojego gita na przykład i odpalasz twoją aplikację (tzn ofc stawiasz to w teście, nie jakoś na boku!). Mockowanie tego nie ma najmniejszego sensu bo nic w ten sposób nie przetestujesz. Tzn no przetestujesz czy mockito i powermock działają co najwyżej :)
Dodatkowo uwaga: Controller nie powinien niczego sprawdzać. Zrób osobny walidator który można przetestować jednostkowo a kontroler zostaw w spokoju.

0

Mam przykładowe repozytorium, którego używam do testów (testy GitClonera).
Mam też przykładowy kod aplikacji (testy DockerImageManagera).
Same testy e2e - niestety odpada - cały proces, od momentu rozpoczęcia klonowania do startu kontenera trwa ponad 20 sekund.

Jeżeli będę chciał przetestować endpointy na:

  • tworzenie aplikacji
  • ręczne przebudowanie aplikacji
  • automatyczne przebudowanie

to już czekam ponad minutę. A do tego dojdzie jeszcze pewnie kilka innych rzeczy. To dopiero początek projektu.

Poza tym powiedzmy, że dodaję nową jakąś funkcję i dostaję błąd. I teraz śledztwo, gdzie siedzi problem. Czy to jakaś springowa adnotacja nie zadziałała tak jak się spodziewałem, czy może źle obsługuję ścieżki lub porty w glue-code :P Może jedno i drugie jednocześnie. Mając testy jednostkowe śpię spokojnie (duże prawdopodobieństwo, że nie ma ukrytych błędów). Testowanie wszystkiego przez e2e jest mniej wygodne i czasochłonne :(

screenshot-20190716220902.png
https://textik.com/#e5e6f324fc9f2999
Mamy 2 grupy testów:

  • Testy 2 testowanie podstaw działania systemu, niezależne od reszty, bardzo wolne
  • Testy 1 testowanie całego systemu, używają klas testowanych w grupie 2. Mogą być szybkie, o ile grupa 2 zostanie w nich (na przykład) zamockowana

Czy to już jest testowanie mockito? :(
Wiadomo, nie jest to kosmicznie skomplikowany system, ale są miejsca w których można popełnić błędy. Chciałbym mieć testy, które będą mi pomagać i przyspieszać rozwój aplikacji. A nie tylko mówić "ups, nie działa, a co dokładnie się dzieje, to znajdź pan sobie sam".

Dodatkowo uwaga: Controller nie powinien niczego sprawdzać. Zrób osobny walidator który można przetestować jednostkowo a kontroler zostaw w spokoju.

Dokładnie tak, kontroler korzysta z przetestowanego walidatora.

2

Testuje się logikę aplikacji, szczególnie w przypadku testów jednostkowych. Ale w unit testach zwykle jakieś względnie małe kawałki. Robienie testu "jednostkowego" który udaje e2e tylko mockuje wszystko po drodze nie ma sensu. Testowanie "kontrolera" w ogóle nie ma sensu poza testami integracyjnymi / e2e bo nie ma w nim logiki. Trudno coś więcej powiedzieć, ale jeśli u ciebie ten AppManager robi po prostu gitCloner.clone(X) to nie ma tu żadnej logiki do testowania. Możesz sobie zrobić jeden sanity test czy dobrze przekazujesz parametry najwyżej.

Inną kwestia jest

Ich obiekty są tworzone i używane wewnątrz AppManagera i na zewnątrz nikt o nich nie wie

Czemu nie masz tam dependency inversion? Czemu AppManager tworzy te obiekty a nie dostaje w konstruktorze? Zawsze jak masz w klasie new... na polach pojawia się problem z testowaniem bez tej zalezności, bo nie ma jak jej podmienić (inaczej niż hardkorowym expectNew). To mi wygląda na błąd w projekcie. Oczywiście nie mam tu na myśli koniecznie robienia z nich jakichś beanów i wstrzykiwania Springiem, mówie po prostu o odwróceniu zależności.

0
Shalom napisał(a):

Testuje się logikę aplikacji, szczególnie w przypadku testów jednostkowych. Ale w unit testach zwykle jakieś względnie małe kawałki.

Ok, a jeżeli mam kod, który "powinien" działać, ale wpadłem jakąś pułapkę adnotacji - i bum, a adnotacja zapewne nie jest objęta definicją logiki.
Tak samo operacje IO. Łatwo coś wywalić. Albo korzystanie z biblioteki, która ma niezbyt dobrą dokumentację.

Robienie testu "jednostkowego" który udaje e2e tylko mockuje wszystko po drodze nie ma sensu. Testowanie "kontrolera" w ogóle nie ma sensu poza testami integracyjnymi / e2e bo nie ma w nim logiki.

Ok nie mamy logiki. Ale jakaś adnotacja może się wysypać, albo zapomnę podać parametru do buildera. Takich rzeczy się nie testuje?
No i nie chcę mockować wszystkiego po drodze tylko samą końcówkę systemu (Gity i Dockery).

Przykład: dodaję endpoint do kontrolera, zapuszczam jego test. Jeżeli będę miał mocki tego, co już działa - to dostaję natychmiast odpowiedź, czy to, co przed chwilą dopisałem też działa. A tak 20 sekund czekania. Niefajnie.

Trudno coś więcej powiedzieć, ale jeśli u ciebie ten AppManager robi po prostu gitCloner.clone(X) to nie ma tu żadnej logiki do testowania. Możesz sobie zrobić jeden sanity test czy dobrze przekazujesz parametry najwyżej.

Z perspektywy AppManagera wygląda to prawie tak jak mówisz. GitCloner ma w sobie "logikę", o ile można to tak nazwać. Nie jest to logika w sensie algorytmicznym, a bardziej mechanicznym. O ile to dobre porównanie. Jak zwał tak zwał. Jest realne ryzyko błędów dlatego myślę, że dobrze mieć testy. Przecież jak to pisałem, to nie wszystko działało za pierwszym razem. A myślenie w TDD ułatwia mi tworzenie lepszego kodu, przynajmniej mam takie odczucie :)

Inną kwestia jest

Ich obiekty są tworzone i używane wewnątrz AppManagera i na zewnątrz nikt o nich nie wie

Czemu nie masz tam dependency inversion? Czemu AppManager tworzy te obiekty a nie dostaje w konstruktorze? Zawsze jak masz w klasie new... na polach pojawia się problem z testowaniem bez tej zalezności, bo nie ma jak jej podmienić (inaczej niż hardkorowym expectNew). To mi wygląda na błąd w projekcie. Oczywiście nie mam tu na myśli koniecznie robienia z nich jakichś beanów i wstrzykiwania Springiem, mówie po prostu o odwróceniu zależności.

AppManager miał być takim samodzielnym magicznym pudełkiem, które nie wymaga specjalnego traktowania i zrealizuje swoje zadania.
Nie myślałem o przekazywaniu tych obiektów przez konstruktor - ukryłem wewnątrz AppManager to, o czym inni nie muszą wiedzieć.
GitCloner nawet nie jest polem w AppManager, jest tymczasowo tworzony w jednej metodzie. Wstrzykiwanie przez konstruktor wywali to na zewnątrz :/ Nie ma tego dużo, ale jednak. Chociaż to może być rozwiązanie problemu. Czy poza lepszą testowalnością mogą być z tego jakieś dodatkowe korzyści (nie planuję dostarczać innych implementacji tych mechanizmów)?

Trochę poza tematem, fajna by była możliwość zapuszczania testów kontrolerów na 2 sposoby. Jeden "na szybko", z mockami, a drugi z wyłączonymi mockami (prawdziwe e2e), który znajdzie ewentualne, niewyłapane wcześniej, błędy. Zanim testy się skończą, można iść na obiad :D

Mam prośbę. Mógłbyś rzucić okiem na AppManagera? Tam właśnie są tworzone i używane GitCloner, DockerImageManager i DockerContainerManager.

3

ale wpadłem jakąś pułapkę adnotacji

To jest runtime i testuje sie to integracyjnie jak stawiasz całą aplikacje.

Jest realne ryzyko błędów dlatego myślę, że dobrze mieć testy.

No dobra, ale co chcesz testować w takiej "delegacji" ;) Możez sobie na to napisać unit test, który sprawdzi czy wywołałeś odpowiednią metodę i tyle. Nie trzeba do tego robić żadnych złożonych mocków.

o czym inni nie muszą wiedzieć.

Nadal nie muszą, przecież możesz dostarczyc tez jakieś Factory które dla normalnego usera ukrywa złożoność tworzenia tego obiektu, a tworzenie go "ręcznie" jest dla powerusera który rozumie co robi i może faktycznie chce sobie podmienic implementacje.

(nie planuję dostarczać innych implementacji tych mechanizmów)?

No jak to nie? Przecież właśnie dyskutujemy o dostarczeniu innej implementacji, testowej/mockowej ;)

Zanim testy się skończą, można iść na obiad

Chyba trochę przesadzasz. Przecież mówimy tu o IT testach a nie o e2e. Wystarczy że sprytnie sparametryzujesz sobie te swoje obiekty i test będzie leciał sekundy a nie minuty. Np. masz tego managera który czaruje dockerem. Nie musisz koniecznie w IT teście sprawdzać czy docker sie zachowa tak jak oczekujesz. Możesz dać tam np. property które określa ścieżkę do binarki dockera, a w testowych properties dać tam jakiegoś fejka. I teraz jesteś nadal o krok dalej niż taki klasyczny mockowany test, bo faktycznie przetestowałeś odpalanie komend shellowych (a nie tylko wołanie metod na mocku) ale jednocześnie nie czekasz pół godziny aż docker wstanie i cośtam zrobi. Tak samo z tym twoim git clonerem, możesz go w teście zastąpić fejkiem zamiast czekać na sklonowanie jakiegoś repo. Te fejki mogą tylko walidować czy dostały jakąś oczekiwaną komendę, albo w ogóle być jakoś konfigurowalne w trakcie testu.

Tak się generalnie przecież robi IT testy dla mikroserwisów. Nie odpalasz wszystkich potrzebnych serwisów w teście, tylko stawiasz sobie jakieś HTTP mocki które udają serwisy których potrzebujesz i są konfigurowalne żeby odpowiadać w taki czy inny sposób na zapytania. Ale to wymaga sparametryzowania "klientów" do takich serwisów, tak zebyś mógł sobie podmienić ich adres na takiego właśnie lokalnego HTTP Servera.
U ciebie generalnie widziałbym to podobnie.

To wszystko nadal wymaga sensownego e2e który sprawdzy czy te nasze komendy współdziałają z prawdziwymi binarkami, ale takiej podejscie i tak jest daleko do przodu przed mockowaniem tych twoich klas, bo testujesz dużo więcej prawdziwego kodu.

0
Shalom napisał(a):

(nie planuję dostarczać innych implementacji tych mechanizmów)?

No jak to nie? Przecież właśnie dyskutujemy o dostarczeniu innej implementacji, testowej/mockowej ;)

Zanim testy się skończą, można iść na obiad

Chyba trochę przesadzasz. Przecież mówimy tu o IT testach a nie o e2e. Wystarczy że sprytnie sparametryzujesz sobie te swoje obiekty i test będzie leciał sekundy a nie minuty. Np. masz tego managera który czaruje dockerem. Nie musisz koniecznie w IT teście sprawdzać czy docker sie zachowa tak jak oczekujesz. Możesz dać tam np. property które określa ścieżkę do binarki dockera, a w testowych properties dać tam jakiegoś fejka. I teraz jesteś nadal o krok dalej niż taki klasyczny mockowany test, bo faktycznie przetestowałeś odpalanie komend shellowych (a nie tylko wołanie metod na mocku) ale jednocześnie nie czekasz pół godziny aż docker wstanie i cośtam zrobi. Tak samo z tym twoim git clonerem, możesz go w teście zastąpić fejkiem zamiast czekać na sklonowanie jakiegoś repo. Te fejki mogą tylko walidować czy dostały jakąś oczekiwaną komendę, albo w ogóle być jakoś konfigurowalne w trakcie testu.

Tak się generalnie przecież robi IT testy dla mikroserwisów. Nie odpalasz wszystkich potrzebnych serwisów w teście, tylko stawiasz sobie jakieś HTTP mocki które udają serwisy których potrzebujesz i są konfigurowalne żeby odpowiadać w taki czy inny sposób na zapytania. Ale to wymaga sparametryzowania "klientów" do takich serwisów, tak zebyś mógł sobie podmienić ich adres na takiego właśnie lokalnego HTTP Servera.
U ciebie generalnie widziałbym to podobnie.

To wszystko nadal wymaga sensownego e2e który sprawdzy czy te nasze komendy współdziałają z prawdziwymi binarkami, ale takiej podejscie i tak jest daleko do przodu przed mockowaniem tych twoich klas, bo testujesz dużo więcej prawdziwego kodu.

No to plan jest taki:

  1. Wprowadzić DI
  2. W testach wstrzykiwać tą samą implementację, ale ze zmienionym adresem Dockera (Docker działa przez HTTP, to mocno ułatwi sprawę). Podobnie z Gitem, muszę obsłużyć tylko zapytanie klonowania.
  3. Pod wskazanym adresem będzie działał udawany Docker (Sparkjava/JavalinWiremock/...) i oszukiwał, zwracając przygotowane wcześniej odpowiedzi. Koniec z czekaniem kilkanaście sekund na zbudowanie obrazu.

Podczas rozwoju projektu będę mógł na bieżąco używać testów szybkich, a przed commitem testy ciężkie dla pewności.

Dzięki za pomoc :) Jednak da się bez PowerMocka :P

2

A widzisz, jednak sie da! A Powermock, tak jak pisałeś, jeśli musisz go dodać żeby testować swój własny kod, to coś jest mocno popsute. Powermock to jest taka ostatnia deska ratunku jak musisz integrować się z jakimś zrypanym kodem (statiki, wszystko samo tworzy sobie obiekty zamiast dependency inversion, harkodowane property itd) i nie możesz tego kodu zrefaktorować :)

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