Narzędzia i wyzwania w testowaniu aplikacji Android

0

W poprzednim tekście z cyklu “Testowanie jednostkowe w Android” opisałem wyniki ankiety przeprowadzonej w celu zbadania, jak wygląda rzeczywistość testowania jednostkowego wśród Android Developerów.

Tym postem chcę zapoznać Cię z wyzwaniami czyhającymi na Android Developerów. Dodatkowo przedstawię najpopularniejsze narzędzia wykorzystane w testowaniu jednostkowym.

Wyzwania

Wydawać by się mogło, że skoro aplikacje Android pisane są w Javie, to czym różni się tworzenie do nich testów od testów pisanych do innych aplikacji w języku Java (backend czy desktop)? Przecież to dalej kod Java, dodatkowo wykorzystujący dostarczone SDK do tworzenia komponentów aplikacji, widoków, itp!

Jaki może być więc problem w testowaniu takiego kodu?

Android SDK

Podstawowym problemem przy testach jednostkowych w Android jest fakt, że próbując przetestować kod opierający się na jakichkolwiek klasach pochodzących z Android SDK bez dodatkowych narzędzi, spotkamy się z głębokim rozczarowaniem. Klasy i metody używane podczas pisania kodu (te, które widzi nasze IDE) są tak naprawdę tylko kopią SDK – jedną wielką zaślepką, bez implementacji i jakiegokolwiek zachowania. Nawet prosta klasa pokroju android.graphics.Rect spłata nam figla w teście jednostkowym, pomimo że jest to tak naprawdę opakowanie na 4 liczby całkowite. Jakiekolwiek przypisanie wartości, do którejkolwiek z jej składowych i tak zawsze skończy się obiektem zawierającym w każdym polu 0! Jeżeli mi nie wierzysz, spróbuj napisać w projekcie Android za pomocą samego JUnit test, który stworzy obiekt Rect z wartościami left=5, top=5, right=10, bottom=10 i załóż w nim, że te wartości po odczytaniu będą takie same.

Dopiero uruchomiona na samym urządzeniu, nasza aplikacja korzysta z prawdziwego Android SDK, już w pełni zaimplementowanego i działającego, jednak na urządzeniu uruchamiamy dopiero testy UI. Testy UI mają do siebie to, że są wolne, często ich powodzenie zależy od konfiguracji urządzenia, na którym są uruchamiane oraz wymuszają na deweloperach pewne inwestycje, żeby zawsze mieć pod ręką różne urządzenia różnego typu z możliwie najszerszą gamą dostępnych wersji systemu Android.

Alternatywną, lecz dalej inwestycją, byłoby zainwestowanie w jakąś chmurę testową jak np. Amazon Device Farm, Bitbar (dawniej TestDroid) czy Firebase Test Lab.

Cykl życia komponentów

Komponenty aplikacji Android takie jak Activity, Service, BroadcastReceiver czy ContentProvider, są zarządzane i tworzone głównie przez system Android (z pewnym wyjątkiem dla BroadcastReceivera). System odpowiedzialny jest też za sterowanie cyklem życia tych komponentów (onCreate, onDestroy, itp.). Pojawia się więc problem: skąd pobrać instancje naszych własnych komponentów, które chcemy przetestować?

W akapicie wyżej wspomniałem już, że Android SDK obecne na maszynie deweloperskiej jest tylko zaślepką, wydmuszką. Nie jesteśmy więc w stanie w żaden sposób zmusić SDK do wyprodukowania nam instancji takiego komponentu lokalnie.

Tworzenie tych komponentów i sterowanie ich cyklem życia samodzielnie również jest nie najlepszym pomysłem i nawet jeżeli pójdziesz tą ścieżką, uda Ci się stworzyć obiekt np. Activity, to rzeczywistość szybko sprowadzi Cię na ziemię: co, jeżeli musisz w swoim teście podać do metody onCreate(Bundle), obiekt typu Bundle z odpowiednimi wartościami? Jak stworzysz odpowiedni obiekt Intent do zwrócenia z metody getIntent(), ponieważ Twoja aktywność pobiera jakieś parametry uruchomieniowe?

Oczywiście możesz próbować tworzyć zaślepki tych obiektów narzędziami typu Mockito, PowerMock lub podobnymi, ale klas w Android SDK jest bez liku i czas potrzebny na zaślepianie w odpowiedni sposób używanych w projekcie klas może przekroczyć czas samego wytwarzania aplikacji.

Różne wersje systemu

Android SDK z wersji na wersję wprowadza nowe mechanizmy. Stare powoli wycofuje (deprecated) czy zmienia działanie niektórych metod (np. AlarmManager.set() zmieniło logikę od wersji API 19). Dobrze jest mieć pewność, że nasz kod podąża za tymi zmianami, jest na nie gotowy i potrafi działać na pełnej gamie wspieranych wersji systemu.

W scenariuszu, kiedy musielibyśmy testować aplikację tylko na urządzeniach, byłoby to dość utrudnione. Musielibyśmy posiadać wiele urządzeń, najlepiej po jednym z każdą możliwą wersją systemu z zakresu, który wspiera nasza aplikacja (każda ma określone minimalne SDK i docelowe pod kątem, którego jest budowana). Na każdym z tych urządzeń trzeba by było uruchomić lub ręcznie przeprowadzić testy. Nie muszę chyba mówić jak bardzo byłoby to czasochłonne.

Narzędzia

Do testów jednostkowych w aplikacjach Android rozsądnie jest podejść w taki sposób, żeby maksymalnie ograniczyć liczbę klas mających styczność z Android SDK i odseparowaną logikę (biznesową czy prezentacji) testować jednostkowo klasycznym zestawem narzędzi. JUnit, do tego jakieś narzędzie do mockowania jak np. Mockito i ewentualnie zapewnić sobie wsparcie w pisaniu asercji, chociażby biblioteką AssertJ. Pilnując się separacji kodu stricte związanego z komponentami Androidowymi od tego, który faktycznie realizuje jakąś logikę lub odwołuje się do zasobów jak np. REST API, zapewniamy sobie automatycznie możliwość testowania dużej części aplikacji jak zwykłego kodu Java. Nie ma konieczności borykania się z problemami wynikającymi z Android SDK.

Warto poczytać o MVP, MVVM czy Clean Architecture w kontekście Androida – te wzorce naświetlą Ci w jaki sposób można dobrze zorganizować architekturę aplikacji, aby jak największą jej część odseparować od kodu związanego z Androidem.

Pozostaje nadal kwestia: co począć z resztą kodu? Tą do której “zepchniemy” zależność od Android SDK. Co z naszymi komponentami jak Activity, Service czy BroadcastReceiver? Mają pozostać bez testów czy może przerzucić je do testów UI i uruchamiać takowe na urządzeniach?

Na całe szczęście istnieje kompromis w postaci biblioteki Robolectric. Pozwala ona w formie unit testów, testować kod zależny od Android SDK. Dostarcza środowisko uruchomieniowe dla testu jednostkowego, symulujące większość klas Android SDK za pomocą mechanizmu zwanego Shadows. Mało tego, możemy definiować własne klasy Shadow i w ten sposób również załatwić w pewnych przypadkach mockowanie obiektów, zamiast używać w tym celu np. Mockito. Trzeba mieć jednak świadomość dwóch niżej wymienionych różnic między Mockito a Robolectriciem w kontekście ich zastosowania przy mockach i na ich podstawie podjąć odpowiednią decyzję o użyciu Mockito, Robolectrica lub obu, w zależności od konkretnej sytuacji.

  1. Mockito pozwala nam stworzyć automatycznie mockową implementację, praktycznie używając jednej linijki kodu. W Robolectric samy musimy dostarczyć logikę mocka. W dużym skrócie Robolectric w porównaniu do Mockito samodzielnie nie zbierze nam informacji o wywołaniach metod czy interakcji z danym mockiem.
  2. Sami musimy zadbać o dostarczenie mocków utworzonych za pomocą Mockito dla kodu testowego. Robolectric zdejmuje z nas ten obowiązek. Ingeruje on na tyle mocno w bytecode, że operator new zwraca obiekt danej klasy otoczony już odpowiednim, zdefiniowanym przez nas lub twórców Robolectrica, obiektem Shadow. Oczywiscie należy pamiętać, że to niekoniecznie musi być zaleta, bo za bardzo kusi nas łamaniem reguły IoC/DIP w testowanym kodzie.

Robolectric rozwiązuje wszystkie problemy wymienione w akapicie Wyzwania. Poza symulowaniem Android SDK w środowisku testu, pozwala na sterowanie cyklem życia komponentów naszej aplikacji oraz na uruchomienie jednego testu pod kątem wielu wersji Android SDK. Robi to bez konieczności kopiowania samego testu!

Narzędziami wymienionymi wyżej będę zajmował się w dalszych częściach cyklu! Zapraszam do czujnego śledzenia bloga.

Nie przegap kolejnych postów!

Dołącz do ponad 9000 programistów w devstyle newsletter!

Tym samym wyrażasz zgodę na otrzymanie informacji marketingowych z devstyle.pl (doh...). Powered by ConvertKit
Share.

About Author

Senior Software Developer i Team Leader we wrocławskim Objectivity. Działam też jako freelancer, szkolę ludzi z Javy oraz Androida, rozwijam swój warsztat blogowy na Medium i czasem zdarzy mi się wystąpić na meetupie czy jako gość w podcastach. W planach mam małą "zdradę" Androida na rzecz fullstack/backend developmentu w Javie i organizacje własnych szkoleń.

Leave A Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.