Testy jednostkowe są tematem, do którego cały czas szukam właściwego podejścia. Bardzo dobrze zdaję sobie sprawę, że w sprawnie działającej aplikacji są niemal niezbędne (przekonałem się o tym w praktyce) i jednocześnie bardzo mnie one fascynują, jednak niezwykle trudno jest odnaleźć właściwy sposób na pisanie testów. W tym artykule też nie dam na to odpowiedzi. Postaram się jedynie przedstawić ogólny zarys całości w odniesieniu do Androida.

Cały czas zastanawia mnie jak do tej kwestii podchodzą różne firmy w naszej branży. Sam zbyt wielkiego doświadczenia nie mam, a odpowiedzi, które dostaję od kolegów programistów są jakieś takie rozmyte 😝 Muszę przyznać, że dla mnie testy są bardzo trudne do ogarnięcia i cały czas mam wrażenie, że biję głową w mur. Dlatego piszę ten artykuł – mam nadzieję, że w trakcie jego przygotowania, niektóre rzeczy jakoś mi się poukładają 😎.

W Androidzie sprawa z testami jeszcze bardziej się komplikuję, bo mamy tu do czynienia z pewnymi, nie zawsze oczywistymi, podziałami.

 

 

 

Testy instrumentalne oraz lokalne

 

 

Podstawowy podział dokonywany jest na dwie kategorie:

testy instrumentalne – testy te muszą być wykonywane na fizycznym urządzeniu lub emulatorze, a to dlatego, że wykorzystują biblioteki Androida. Z uwagi na to są bardzo powolne i powinny być wykorzystywane z rozwagą. Testy te zaliczają się do kategorii testów Integracyjnych oraz UI (więcej o tym będzie za chwilę). Ich nazwa pochodzi od Android Instrumentation API, które pozwala na manualne zarządzanie komponentami aplikacji oraz cyklami ich życia. Chyba najbardziej popularną biblioteką do testów instrumentalnych jest Espresso, które przeznaczone jest głównie do symulowania interakcji użytkownika z aplikacją. Dzięki niej możecie na przykład napisać kod, który po włączeniu aplikacji będzie wprowadzał testowe dane w pola email / hasło, a następnie będzie klikał w przycisk „Zaloguj”.

Po całej operacji będziecie mogli sprawdzić, czy w przypadku prawidłowego logowania ładowany jest ekran zawierający treści dla zarejestrowanego użytkownika. Podobne zastosowanie ma biblioteka UIAutomator. I tutaj jak dla mnie powstaje trochę zamieszania. W większości artykułów, które czytałem pomija się fakt, że testy instrumentalne bardzo często są zwykłymi testami jednostkowymi, które testują naszą logikę biznesową w zestawieniu z bibliotekami Androida. Zwykle wspomina się wyłącznie o testach integracyjnych oraz UI, a pomija się fakt, że najbardziej podstawowe testy jednostkowe również mogą być zaliczane do tej kategorii. Moim zdaniem jest to sporo niedopatrzenie.

– lokalne testy jednostkowe – testy te uruchamiane są lokalnie na naszej maszynie (komputerze) z pomocą środowiska Java Virtual Machine (JVM) i nie potrzebują ani fizycznego urządzenia z Androidem, ani emulatora. A to dlatego, że nie wykorzystują one w żaden sposób bibliotek Androida. Dzięki temu są one znacznie szybsze i powinny stanowić przeważającą część naszych testów (szczegóły poniżej).

Z ich pomocą, co oczywiste, możemy testować wyłącznie kod naszego autorstwa, ale jeżeli bardzo chcemy symulować biblioteki Androida to możemy skorzystać z Robolectic lub PowerMock. Zwykle korzysta się z nich jednocześnie, ponieważ dobrze się uzupełniają. Do pisana testów jednostkowych wykorzystuje się bibliotekę o nazwie JUnit, ale żeby była jasność – testy jednostkowe nie muszą być tylko lokalne. Jeżeli będą one korzystały z bibliotek Androida to wtedy to przekształcą się w testy instrumentalne. Jak dokładnie to wygląda możecie sprawdzić pod tym linkiem.

Jak sami widzicie temat jest trochę zakręcony, a to jeszcze nie wszystko. Podziałów jest więcej i trudność w ich zrozumieniu polega na tym, że bardzo często ich granice na siebie nachodzą.

 

 

Piramida testowa

 

 

Istnieje również podział, który nie jest powiązany ściśle z platformą Android, a bardziej z testami na platformy mobilne w ogólnym ujęciu. Mowa tutaj o tzw. piramidzie testowej:

 

 

Źródło: The 3 tiers of the Android test pyramid

 

 

Testy UIpowinny stanowić 10 % wszystkich testów – tutaj głównymi bibliotekami są Espresso, UI Automator oraz Robotium. Testy te są odpowiedzialne za sprawdzenie poprawności działania elementów UI w naszej aplikacji. Dzięki wspomnianym bibliotekom jesteśmy w stanie za pomocą kodu symulować zachowania potencjalnego użytkownika aplikacji. Możemy na przykład wypełnić pola text view danymi do logowania, kliknąć w przycisk „zaloguj”, a następnie zweryfikować, czy pojawia się „pop up” z właściwą informacją. Tego typu testy są bardzo powolne ponieważ wymagają za każdym razem użycia emulatora lub fizycznego urządzenia. W trakcie kompilacji tworzony jest osobny, testowy APK, który działa jednocześnie z właściwym APK naszej aplikacji. Z uwagi na fakt, że testy te wymagają dostępu do bibliotek Androida zaliczają się one do wcześniej opisanych testów instrumentalnych.

Testy Integracyjne – powinny stanowić 20 % wszystkich testów – testy te są przeznaczone do weryfikowania poprawności komunikacji pomiędzy poszczególnymi komponentami w naszej aplikacji. Może to być współpraca pomiędzy obiektami tylko naszego autorstwa lub na przykład pomiędzy naszym komponentem a obiektem z biblioteki Android.

I tutaj moim zdaniem powstaje sporo zamieszania, ponieważ w mojej opinii testy te w zależności od wykorzystania można zaliczyć do testów instrumentalnych lub lokalnych. Na czym polega różnica? Jeżeli w ramach testów będziemy sprawdzali współpracę naszego komponentu z komponentem z biblioteki Android, a przy tym nie będziemy korzystali z żadnej biblioteki, która symuluje (mockuje – wyjaśnienie terminu poniżej) zachowania implementacji Androidowych (jak na przykład Robolectic), to w takim przypadku testy te będą zaliczane do Instrumentalnych. Zgodnie z tym co napisałem powyżej, konieczne będzie ich uruchomienie na emulatorze lub urządzeniu fizycznym.

Jeżeli jednak będziemy testować współpracę dwóch komponentów naszego autorstwa lub współpracę z bibliotekami Androida, ale zastąpionymi mockami, to w w takim przypadku testy te zaliczać będziemy do testów lokalnych, czyli takich, które nie wymagają emulatora, ani urządzenia fizycznego (są wykonywane w środowisku JVM). Własnie takie testowanie jest zalecane. Ogólnie nie ma większego sensu testowanie samych bibliotek Androida, bo to zadanie inżynierów z Googla 😜 Powinniśmy testować jedynie jak nasze obiekty z nimi współpracują.

Testy Jednostkowe (Unit Tests) – powinny stanowić 70 % wszystkich testów – są to testy przeznaczone dla pojedynczych modułów w naszej aplikacji, tzw. SUT – Sytem Under Test. W tym przypadku zakładamy, że wszystkie inne moduły, z którymi współpracuje moduł testowany, zostały sprawdzone (najlepiej za pomocą innego zestawu testów jednostkowych) i możemy je spokojnie zastąpić mockami. Testy te są bardzo dobre do sprawdzania logiki biznesowej, czyli tego co nasza aplikacja powinna w rzeczywistości robić. To właśnie miedzy innymi z uwagi na testy jednostkowe (i testy ogólnie) bardzo ważne jest, aby pisać krótkie metody, które będą skupiały się na wykonywaniu tylko jednej rzeczy. Takie podejście zwyczajnie ułatwia pisanie testów.

Bardzo często w różnych artykułach podawane jest, że w przypadku testów jednostkowych korzysta się głownie z biblioteki Mockito oraz JUnit, ale ta druga wykorzystywana jest również w testach Integracyjnych (do wykonywania asercji). Testy jednostkowe mogą być zarówno instrumentalne jak i lokalne, w zależności do tego, czy będą potrzebowały dostępu do bibliotek Androida. W niektórych artykułach podawane jest, że testy jednostkowe zaliczają się tylko do testów lokalnych, ale jak wynika z dokumentacji (tutaj link) możliwe jest również tworzenie instrumentalnych testów jednostkowych.

 

 

Mock

 

 

Mockowanie polega na zastąpieniu pewnego obiektu innym obiektem, który będzie tylko „udawał”, że jest obiektem oryginalnym. Taki prosty przykład – załóżmy, że mamy klasę NetworkHelper, w której znajduje się metoda o nazwie getAllAnimals(). Odpowiedzialna jest ona za połączenie się z serwerem i pobranie z niego danych o wszystkich zwierzętach w bazie (powiedzmy, że jest to aplikacji przeznaczona dla schronisk 😌). Teraz załóżmy, że mamy klasę AnimalPresenter, która wykorzystuje obiekt NetworkHelper i jego funkcję getAllAnimals() w celu wyświetlenia danych o zwierzętach na ekranie urządzenia. Chcemy napisać testy dla naszej klasy AnimalPresenter, które sprawdzą, czy po pobraniu danych o zwierzętach, dane te są właściwie deserializowane (zamieniane na właściwe obiekty). Łączenie się w tym celu za każdym razem z serwerem mija się z celem, ponieważ wynik testu będzie mocno uzależniony od aktualnej dostępności serwera. A przecież my chcemy tylko sprawdzić, czy dane są odpowiednio parsowane i przekazywane dalej! I tutaj właśnie przydają się mocki. Tworzymy obiekt, który będzie tylko udawał prawdziwą klasę NetworkHelper, a każde wywołanie jego funkcji getAllAnimals() będzie zwracało na sztywno zdefiniowaną kolekcję zwierzaków. Takie użycie funkcji nazywamy stubbing, czyli tworzenie stałego, przewidywalnego i powtarzalnego wyniku działania funkcji. Dzięki tym zabiegom uniezależnimy się od kaprysów naszego połączenia sieciowego. Oczywiście w przypadków testów pisanych dla samej klasy NetworkHelper, będziemy musieli skorzystać już z oryginalnej implementacji.

 

 

Stan vs Zachowanie

 

 

Żebyście mieli w miarę pełny obraz, na koniec krótki opis różnicy między weryfikacją stanu (state) a zachowania (behavior). W przypadku weryfikacji stanu pierwsza faza polega na przygotowaniu wszystkich zależności, na których będzie polegał testowany obiekt. Następnie wykonywane są założone operacje, a na końcu sprawdzane jest, czy stan obiektu zgodny jest z przewidywaniami. Wykonywane są tutaj asercje, dlatego najczęściej wykorzystywaną w tym przypadku biblioteką jest JUnit.

Podczas weryfikacji zachowania określane jest jakie metody danej zależności (obiektu) powinny zostać wywołane, a następnie weryfikuje się, czy tak właśnie się stało. Tutaj bardzo przydatna jest biblioteka Mockito.

 

FIN

 

Jak sami widzicie sprawa nie jest wcale taka prosta. Podejrzewam, że znajdzie się kilka osób, które nie zgodzą się z przedstawionym przez mnie podziałem. Traktujcie to tylko jako wstęp. W kolejnych wpisach postaram się w większym stopniu skupić się na poszczególnych rodzajach testów oraz towarzyszących im bibliotekach takich jak Mockito, JUnit, Robolectic, czy Espresso. Do usłyszenia 😎.

 

 


 

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *