Wstęp
Testowanie jednostkowe kodu Androidowego od długiego czasu wzbudza wiele emocji ze względu na mocną zależność od Android SDK, które skutecznie podstawia programistom kłody pod nogi. Główną przyczyną jest budowa Android SDK, które w dużej mierze bazuje wszędzie na klasie Context
a na domiar złego większość klas z Android SDK nie jest interfejsem tylko konkretną implementacją z ukrytym konstruktorem lub oznaczoną jako final
. Na szczęście istnieją biblioteki, które pozwalają na stworzenie mocków nawet klas finalnych.
Czego się nauczysz
- konfigurować projekt do testów jednostkowych
- pisać testy jednostkowe do swojego kodu
- używać biblioteki do mockowania MockK
Konfiguracja środowiska:
Jeśli utworzyłeś projekt w AndroidStudio to najprawdopodobniej w swoim projekcie masz już katalog z testami app/src/test/java
i przykładowym testem, który możesz usunąć.
Jeśli nie masz takiego katalogu to stwórz app/src/test/java
i wewnątrz dodaj pakiet odpowiadający projektowi – w tym artykule jest to pl.androidcoder.unittest
. Powinno wyglądać to tak:

Następnie sprawdź czy w pliku app/build.gradle
masz zależność do biblioteki junit
:
dependencies { ... testImplementation 'junit:junit:4.12' }
Jeśli wszystko zrobiłeś to uruchom wbudowany w Android Studio terminal (Option+F12
lub Alt+F12
) i wpisz polecenie:
Dla Windows:
gradlew testDebugUnitTest
Dla Linux i MacOS:
./gradlew testDebugUnitTest
Jeśli poprawne wszystko skonfigurowałeś to powinieneś otrzymać poniższy komunikat:
BUILD SUCCESSFUL in 19s 15 actionable tasks: 13 executed, 2 up-to-date
Przykład 1: Test metody z rodziny utils
Jeśli klasa, którą testujesz nie ma zależności od żadnej implementacji z Android SDK to testy jednostkowe nie będą się różniły od testów jednostkowych w Javie/Kotlinie.
Pierwszym testem jaki napiszesz będzie test funkcji removeDuplicates
, która usuwa duplikujące się obiekty w kolekcji:
package pl.androidcoder.unittest.utils fun <T> removeDuplicates(list : List<T>) : List<T>{ val noDuplicates = mutableListOf<T>() list.forEach { if(!noDuplicates.contains(it)){ noDuplicates.add(it) } } return noDuplicates }
Utworzenie pliku z testem
Tworzenie pliku z IDE
Żeby stworzyć nowy plik z testem ustaw kursor na nazwę metody, do której chcesz napisać test, naciśnij kombinację cmd+shift+t
lub ctrl+shift+t
i wybierz Create New Test

Upewnij się że w okienku, które się pojawi jako Testing library
masz wybrany jUnit4. W Class name
usuń Kt
żeby klasa nazywała sie CollectionUtilsTest
. (jest to spowodowane tym, że funkcja znajduje się bezpośrednio w pliku kotlinowym (a nie w klasie) , które ma rozszerzenie Kt
).

Po zatwierdzeniu pojawi się jeszcze jedno okno z wyborem lokalizacji nowego pliku. Koniecznie wybierz app/src/test
, ponieważ umiejscowienie pliku w złym katalogu spowoduje, że nie zostanie uruchomiony.

Ręczne tworzenie pliku
Możesz też utworzyć ręcznie plik CollectionUtilsTest.kt
w katalogu app/src/test/java
w pakiecie pl.androidcoder.unittest.utils
.
Uruchamianie testów
Tak powinien wyglądać nowo utworzony plik dla testów:
package pl.androidcoder.unittest.utils import org.junit.Assert.* class CollectionUtilsTest
Żeby gradle wykrył, że klasa CollectionUtilsTest
zawiera testy to musi mieć przynajmniej jedną funkcję oznaczoną adnotacją @Test
:
package pl.androidcoder.unittest.utils import org.junit.Assert.* import org.junit.Test class CollectionUtilsTest{ @Test fun first(){ } }
Po każdorazowym dodaniu nowego testu uruchom pusty test. Jeśli test nie przejdzie to znaczy, że problem leży gdzieś w konfiguracji. Uruchamianie pustych testów ochroni Cię przed złą interpretacją nieprzechodzącego testu – sam kilka razy wpadłem w tę pułapkę i straciłem masę czasu szukając błędu tam gdzie go nie było.
Uruchamianie testu z IDE
Test możesz uruchomić z interfejsu AndroidStudio naciskając mały trójkącik po lewej stronie przy nazwie klasy lub metody (ten przy metodzie uruchamia jedynie tę metodę, ten przy klasie uruchamia wszystkie testy z klasy.

Polecam uruchamianie testów z IDE, jeśli aplikacja ma bardzo duży zestaw testów i chcesz uruchomić tylko jeden test lub grupę testów ze specyficznego pakietu. Wyniki testów uruchamianych za pomocą IDE są bardziej czytelne od tych z konsoli.

BUILD SUCCESSFUL in 14s 15 actionable tasks: 5 executed, 10 up-to-date
Uruchamianie testu z terminala
Jeśli jednak lubisz konsolę lub chcesz szybko uruchamiać wszystkie testy to użyj poniższych poleceń w terminalu:
Dla Windows:
gradlew testDebugUnitTest
Dla Linux i MacOS:
./gradlew testDebugUnitTest
aby zwiększyć czytelność wyników testów dodaj w pliku build.gradle
poniższy kod:
android{ ... } tasks.withType(Test) { testLogging { events "started", "passed", "skipped", "failed" } }
dzięki niemu zostaną wypisane na ekranie wszystkie uruchomione testy oraz ich wynik:
> Task :app:testDebugUnitTest pl.androidcoder.unittest.utils.CollectionUtilsTest > first STARTED pl.androidcoder.unittest.utils.CollectionUtilsTest > first PASSED BUILD SUCCESSFUL in 3s 15 actionable tasks: 15 executed
Testy metody removeDuplicates
Skoro cała konfiguracja już działa, to możesz napisać test. Pisanie testów zacznij od najbardziej zdegenerowanych przypadków jakie istnieją, zostawiajac na sam koniec pozytywny przebieg. Najbardziej zdegenerowanym przypadkiem dla metody removeDuplicates
jest przekazanie w argumencie pustej listy.
Test 1: Pusta lista
Jeśli metoda removeDuplicates
dostanie pustą listę to powinna zwrócić pustą listę.
class CollectionUtilsTest{ @Test fun `given empty list removeDuplicates should return empty list`(){ //ARRANGE val emptyList = emptyList<String>() //ACT val actualList = removeDuplicates(emptyList) //ASSERT assert(actualList.isEmpty()) {"List should be empty but has ${actualList.size} elements"} } }
Struktura testu
Test składa się z 3 głównych bloków ARRANGE ACT ASSERT (lub wywodzący się z metodyki BDD : GIVEN WHEN THEN)
- ARRANGE (Given)- w tym bloku są ustawiane wszystkie warunki początkowe dla testu, w powyższym przykładzie jest to przygotowanie pustej listy.
- ACT (When)- wywołanie metody do testowania, zazwyczaj znajduje się tutaj tylko jedna linia. Wynik działania funkcji przechowuję w zmiennej z przedrostkiem
actual*
, dla rozróżnienia wartości wyjściowej testu od wartości wejściowych. - ASSERT (Then)- sprawdzanie wyniku działania testu, w bloku ASSERT powinno być jedno logiczne sprawdzenie tzn. może być wiele linii kodu ale musi sprawdzać jedną rzecz (w powyższym kodzie jest to metoda isEmpty()). Metoda
assert
jako pierwszy argument przyjmuje wartośćBoolean
jeśli jestfalse
to test kończy się niepowodzeniem i zostaje wyświetlona wiadomość podana jako drugi argument w formie lambdy{
"List should be empty but has ${actualList.size} elements"
}
. Dodawanie wiadomości do asercji jest bardzo ważne, ponieważ bardzo szybko pozwala wykryć błąd:

Warto dodawać komentarze oddzielające te bloki. Komentarze dają też psychologiczne ograniczenie do tworzenia testu niezgodnego z tym schematem.
Aby zwiększyć czytelność wyekstraktuj asercje do osobnych metod jeśli są troszkę bardziej skomplikowane:
class CollectionUtilsTest{ @Test fun `given empty list removeDuplicates should return empty list`(){ //ARRANGE val emptyList = emptyList<String>() //ACT val actualList = removeDuplicates(emptyList) //ASSERT assertEmptyList(actualList) } private fun <T> assertEmptyList(actualList: List<T>) { assert(actualList.isEmpty()) { "List should be empty but has ${actualList.size} elements" } } }
metoda assertEmptyList
jest bardziej czytelna. Niekiedy asercje mogą mieć kilka linijek, skomplikowane pętle itp. Taka duża ilość kodu w teście potrafi skutecznie zaciemnić znaczenie.
Test 2: Wartości null
Jeśli metoda removeDuplicates
dostanie listę z wartościami null to powinna zwrócić pustą listę.
@Test fun `given list with nulls removeDuplicates should return empty list`(){ //ARRANGE val emptyList = listOf<Any?>(null, null, null) //ACT val actualList = removeDuplicates(emptyList) //ASSERT assertEmptyList(actualList) }
W powyższym przypadku otrzymałem błąd:

Dzięki opisowi asercji zobaczysz od razu co jest problemem. Funkcja, która usuwa duplikaty traktuje wartość null
jako jeden z elementów listy. Żeby test przeszedł, musisz zmodyfikować funkcję tak aby pomijała wartości null
:
fun <T> removeDuplicates(list : List<T>) : List<T>{ val noDuplicates = mutableListOf<T>() list.forEach { if(it != null && !noDuplicates.contains(it)){ noDuplicates.add(it) } } return noDuplicates }

Jak widzisz testy pozwalają wychwycić pewne błędy, których nie przetestowaliśmy manualnie. Czasami jako początkujący programiści nie wiemy jak jakaś funkcja się zachowuje.
Kiedyś miałem mały projekt, w którym w funkcji main
testowałem działanie jakiś bibliotek po to aby nie testować na wielkim kodzie w dodatku na urządzeniu. Gdybym wtedy umiał pisać testy jednostkowe mógłbym zaoszczędzić mnóstwo czasu.
Test 3: Wszystkie elementy takie same
Jeśli w liście wszystkie elementy są takie same to zwróć listę z jednym elementem
@Test fun `given list the same elements removeDuplicates should return list with one element`() { //ARRANGE val list = listOf(TestData("a"), TestData("a"), TestData("a")) val expectedList = listOf(TestData("a")) //ACT val actualList = removeDuplicates(list) //ASSERT assertEquals(expectedList, actualList) }

Użyłem funkcji assertEquals
, która przyjmuje jako pierwszy parametr wynik oczekiwany a jako drugi wynik aktualny. Funkcje assertEqual
,assertNotEquals
, assertArrayEquals
, assertNull
, assertSame
znajdziesz w klasie org.junit.Assert
jako metody statyczne.
Test 4: Lista bez powtórzeń
Jeśli lista jest bez powtórzeń to zwróć taką samą listę
@Test fun `given list with unique elements removeDuplicates should return the same list`() { //ARRANGE val expectedList = listOf(TestData("a"), TestData("b"), "first") //ACT val actualList = removeDuplicates(expectedList) //ASSERT assertEquals(expectedList, actualList) }

Test 5: Lista z powtórzeniami
Jeśli w liście powtarzają się elementy to zwróć listę bez powtórzeń
@Test fun `given list with duplications removeDuplicates should return list without duplications`() { //ARRANGE val list = listOf(TestData("a"), TestData("a"), TestData("a"), "first", "first", "second") val expectedList = listOf(TestData("a"), "first", "second") //ACT val actualList = removeDuplicates(list) //ASSERT assertEquals(expectedList, actualList) }

Test 6: Nie modyfikuj listy
Funkcja nie może modyfikować listy przekazanej jako argument
@Test fun `given list with duplications removeDuplicates should not edit given list`() { //ARRANGE val list = listOf(TestData("a"), TestData("a"), TestData("a"), "first", "first", "second") val expectedList = list.toList() //copy list //ACT removeDuplicates(list) //ASSERT assertEquals(expectedList, list) }

Podsumowanie przykładu
Prawie w każdym projekcie istnieje klasa Utils
ze statycznymi metodami, które są wykorzystywane do prostych i niezmiennych w czasie czynności takich jak formatowanie, konwersje, operacje na kolekcjach itp. Takie funkcje są najlepszym materiałem na naukę podstaw pisania testów jednostkowych. Jeśli nie masz takich funkcji w projekcie to w ramach ćwiczeń pobierz repozytorium apache-commons-lang, usuń istniejące testy i napisz je od nowa. Przy okazji zajrzyj w istniejące tam testy a nauczysz się różnych mechanizmów stosowanych podczas testowania.
Przykład 2: Interakcja z BatteryManager
W tym przykładzie pokażę w jaki sposób napisać testy w przypadku gdy klasa ma zależności od Android SDK – tutaj od BatteryManager
. Kluczem do sukcesu testowania tą metodą jest unikanie tzw. „train wreck” np. fragment.context.getService(Context.BATTERY_SERVICE)
. Należy użyć wstrzykiwania w konstruktorze obiektu klasy BatteryManager
tak jak ma to miejsce w BatteryInfoFactory
:
class BatteryInfoFactory(private val batteryManager: BatteryManager) { companion object { private const val DATA_NOT_AVAILABLE = Integer.MIN_VALUE } fun getBatteryCurrent(): BatteryCurrent? { val actualCurrent = getActualCurrent() val averageCurrent = getAverageCurrent() val actualCharge = getActualCharge() return if (allNull(actualCurrent, averageCurrent, actualCharge)) null else BatteryCurrent(actualCurrent, averageCurrent, actualCharge) } private fun allNull(vararg variables: Any?) = variables.all { it == null } private fun getActualCharge(): AmpereHour? { val actualChargeProperty = batteryManager.getIntProperty(BATTERY_PROPERTY_CHARGE_COUNTER) return if (actualChargeProperty != DATA_NOT_AVAILABLE) AmpereHour.fromMicroAmpereHour(actualChargeProperty) else null } private fun getAverageCurrent(): Ampere? { val currentAverageProperty = batteryManager.getIntProperty(BATTERY_PROPERTY_CURRENT_AVERAGE) return if (currentAverageProperty != DATA_NOT_AVAILABLE) Ampere.fromMicroAmpere(currentAverageProperty) else null } private fun getActualCurrent(): Ampere? { val currentNowProperty = batteryManager.getIntProperty(BATTERY_PROPERTY_CURRENT_NOW) return if (currentNowProperty != DATA_NOT_AVAILABLE) Ampere.fromMicroAmpere(currentNowProperty) else null } }
data class BatteryCurrent( val actualCurrent: Ampere?, val averageCurrent: Ampere?, val actualCharge: AmpereHour? )
data class Ampere(val value: Double) { companion object { fun fromMicroAmpere(microAmperes: Int): Ampere { return Ampere(microAmperes / 1000000.0) } } }
data class AmpereHour(val value: Double) { companion object { fun fromMicroAmpereHour(microAmperesHour: Int): AmpereHour { return AmpereHour(microAmperesHour / 1000000.0) } } }
Test do BatteryInfoFactory
Stwórz nowy pusty test i uruchom, żeby zobaczyć czy wszystko działa:
class BatteryInfoFactoryTest { @Test fun empty() { } }

Przy tworzeniu instancji BatteryInfoFactory
napotykasz na problem, ponieważ konstruktor wymaga BatteryManager
a obiekt tej klasy można pobrać jedynie z klasy Context
, do której nie ma dostępu z poziomów testów. BatteryManager
nie jest niestety interfejsem więc nie można stworzyć własnej implementacji do testów. Konstruktory są ukryte więc nie można nawet przeciążyć klasy.
Czy już nic nie da się zrobić? Trzeba stworzyć interfejs i delegować wszystkie funkcje?
Oczywiście, że jest rozwiązanie, które ratuje przed nadmiernymi implementacjami i odpuszczeniem testów. Trzeba użyć biblioteki, która potrafi robić mocki klas – czyli dostarcza instancję klasy, która nie ma żadnej funkcjonalności – przesłania jej całą funkcjonalność pozwalając symulować jej stan lub zachowanie.
Bibliotek tego typu jest kilka: Mockito (chyba najpopularniejsze), PowerMock, JMockit, EasyMock i MockK. (być może jakąś pominąłem)
Przez ostatnie lata używałem Mockito jednak nie wspiera oficjalnie składni kotlina – istnieje dodatkowa nieoficjalna biblioteka dodająca składnię kotlinową https://github.com/nhaarman/mockito-kotlin.
W poniższym przykładzie zrezygnują z Mockito i użyję biblioteki MockK, którą pokazał mi znajomy i przypadła mi go gustu w szczególności, że ma pełne wsparcie dla kotlina.
Dodatnie MockK do projektu
Wystarczy, że dodasz tylko jedną zależność. Aby sprawdzić najnowszą wersję wejdź na repozytorium Github projektu MockK https://github.com/mockk/mockk.
dependencies { testImplementation "io.mockk:mockk:1.9.3.kotlin12" }
Stworzenie mocka BatteryManager
Aby stworzyć mocka użyj metody mockk
dostarczonej przez bibliotekę:
class BatteryCurrentTest private val batteryManager = mockk<BatteryManager>() }
Gdy już masz obiekt klasy BatteryManager
to możesz stworzyć także implementację BatteryInfoFactory
:
class BatteryCurrentTest { private val batteryManager = mockk<BatteryManager>() private val factory = BatteryInfoFactory(batteryManager) }
to już jest wszystko, żeby zabrać się za pierwszy test.
Test 1 : Żadna wartość nie jest dostępna
Zaczynam od najbardziej zdegenerowanego przypadku testowego:
Jeśli żadna wartość nie jest dostępna to getBatteryCurrent
zwróci null
.
@Test fun `when all data is unavailable then return null`() { //ARRANGE ???? //ACT val batteryCurrent = factory.getBatteryCurrent() //ASSERT assertNull(batteryCurrent) }
W listingu P2.9 w 4 linii pozostawiłem znaki zapytania. Biblioteki takie jak MockK dają możliwość zakrywania prawdziwej implementacji w taki sposób, że możemy zasymulować co ma zwracać dana metoda. Jeśli wrócisz do listingu 2.1 to zobaczysz, że wszystkie dane pobierane są za pomocą jednej metody batteryManager.getIntProperty()
i z różnymi argumentami: BATTERY_PROPERTY_CHARGE_COUNTER
, BATTERY_PROPERTY_CURRENT_AVERAGE
i BATTERY_PROPERTY_CURRENT_NOW
.
Jeśli w teście na obiekcie batteryManager
wykonamy poniższą operację:
every{ batteryManager.getIntProperty(BATTERY_PROPERTY_CURRENT_NOW) } returns 123
to za każdym razem gdy zostanie wywołana metoda getIntProperty
z argumentem BATTERY_PROPERTY_CURRENT_NOW
zostanie zwrócona liczba 123.
dzięki temu możemy uzupełnić pierwszy test o zmockowane funkcje w BatteryManager
class BatteryCurrentTest { companion object { private const val DATA_NOT_AVAILABLE = Integer.MIN_VALUE } private val batteryManager = mockk<BatteryManager>() private val factory = BatteryInfoFactory(batteryManager) @Test fun `when all data is unavailable then return null`() { //ARRANGE batteryManager.apply { every { getIntProperty(BATTERY_PROPERTY_CURRENT_NOW) } returns DATA_NOT_AVAILABLE every { getIntProperty(BATTERY_PROPERTY_CURRENT_AVERAGE) } returns DATA_NOT_AVAILABLE every { getIntProperty(BATTERY_PROPERTY_CHARGE_COUNTER) } returns DATA_NOT_AVAILABLE } //ACT val batteryCurrent = factory.getBatteryCurrent() //ASSERT assertNull(batteryCurrent) } }

Linie 14-16 Listingu P2.11 zmieniają zachowanie funkcji getPropery()
klasy BatteryManager
w taki sposób, że jeśli w jako parametr podasz BATTERY_PROPERTY_CURRENT_NOW
, BATTERY_PROPERTY_CURRENT_AVERAGE
lub BATTERY_PROPERTY_CHARGE_COUNTER
to zwróci ona wartość DATA_NOT_AVAILABLE
Jeśli metoda getIntProperty
zwróci wartość Integer.
MIN_VALUE
to oznacza, że dana właściwość nie jest dostępna – sprawdź dokumentację BatteryManager
i klasę BatteryInfoFactory
(listing P2.1).
Wydzieliłem stałą Integer.
do innej stałej MIN_VALUE
DATA_NOT_AVAILABLE
aby nadać jej znaczenie i poprawić czytelność kodu.
Test 2-4: Jedna z wartości nie jest dostępna
Jeśli BATTERY_PROPERTY_CURRENT_NOW
nie jest dostępne to getBatteryCurrent
zwróci BatteryCurrent
bez actualCurrent
@Test fun `should return null actualCurrent when is not available`() { //ARRANGE batteryManager.apply { every { getIntProperty(BATTERY_PROPERTY_CURRENT_NOW) } returns DATA_NOT_AVAILABLE every { getIntProperty(BATTERY_PROPERTY_CURRENT_AVERAGE) } returns 222222222 every { getIntProperty(BATTERY_PROPERTY_CHARGE_COUNTER) } returns 333333333 } //ACT val batteryCurrent = factory.getBatteryCurrent() //ASSERT batteryCurrent?.apply { assertNull(actualCurrent) assertEquals(Ampere(222.222222), averageCurrent) assertEquals(AmpereHour(333.333333), actualCharge) } }

Jeśli BATTERY_PROPERTY_CURRENT_AVERAGE
nie jest dostępne to getBatteryCurrent
zwróci BatteryCurrent
bez averageCurrent
@Test fun `should return null averageCurrent when is not available`() { //ARRANGE batteryManager.apply { every { getIntProperty(BATTERY_PROPERTY_CURRENT_NOW) } returns 111111111 every { getIntProperty(BATTERY_PROPERTY_CURRENT_AVERAGE) } returns DATA_NOT_AVAILABLE every { getIntProperty(BATTERY_PROPERTY_CHARGE_COUNTER) } returns 333333333 } //ACT val batteryCurrent = factory.getBatteryCurrent() //ASSERT batteryCurrent?.apply { assertEquals(Ampere(111.111111), actualCurrent) assertNull(averageCurrent) assertEquals(AmpereHour(333.333333), actualCharge) } }

Jeśli BATTERY_PROPERTY_CHARGE_COUNTER
nie jest dostępne to getBatteryCurrent
zwróciBatteryCurrent
bez actualCharge.
@Test fun `should return null actualCharge when is not available`() { //ARRANGE batteryManager.apply { every { getIntProperty(BATTERY_PROPERTY_CURRENT_NOW) } returns 111111111 every { getIntProperty(BATTERY_PROPERTY_CURRENT_AVERAGE) } returns 222222222 every { getIntProperty(BATTERY_PROPERTY_CHARGE_COUNTER) } returns DATA_NOT_AVAILABLE } //ACT val batteryCurrent = factory.getBatteryCurrent() //ASSERT batteryCurrent?.apply { assertEquals(Ampere(111.111111), actualCurrent) assertEquals(Ampere(222.222222), averageCurrent) assertNull(actualCharge) } }

Refaktoryzacja testów
Po dodaniu kilku testów, możesz zauważyć że niektóre bloki kodu powtarzają się. Jeśli wyekstraktujesz je do osobnych metod lub obiektów to poprawisz czytelność testów i przyspieszysz pisanie kolejnych.
W powyższym przykładzie w każdym teście powtarza się blok ARRANGE:
//ARRANGE batteryManager.apply { every { getIntProperty(BATTERY_PROPERTY_CURRENT_NOW) } returns 111111111 every { getIntProperty(BATTERY_PROPERTY_CURRENT_AVERAGE) } returns 222222222 every { getIntProperty(BATTERY_PROPERTY_CHARGE_COUNTER) } returns DATA_NOT_AVAILABLE }
Wyekstraktuj go do osobnej metody z parametrami:
private fun mockBatteryProperties( currentNowValueMicro: Int, currentAverageValueMicro: Int, chargeCounterValueMicro: Int ) { batteryManager.apply { every { getIntProperty(BATTERY_PROPERTY_CURRENT_NOW) } returns currentNowValueMicro every { getIntProperty(BATTERY_PROPERTY_CURRENT_AVERAGE) } returns currentAverageValueMicro every { getIntProperty(BATTERY_PROPERTY_CHARGE_COUNTER) } returns chargeCounterValueMicro } }
Wyekstraktuj także blok assert, ponieważ w każdym teście jest podobny:
private fun assertBatteryCurrent( batteryCurrent: BatteryCurrent?, expectedActualCurrent: Double?, expectedAverageCurrent: Double?, expectedActualCharge: Double? ) { assertNotNull(batteryCurrent) batteryCurrent?.apply { if (expectedActualCurrent != null) assertEquals(Ampere(expectedActualCurrent), actualCurrent) else assertNull(actualCurrent) if (expectedAverageCurrent != null) assertEquals(Ampere(expectedAverageCurrent), averageCurrent) else assertNull(averageCurrent) if (expectedActualCharge != null) assertEquals(AmpereHour(expectedActualCharge), actualCharge) else assertNull(actualCharge) } }
Dzięki tym zabiegom kod zyskał na czytelności i jego objętość znacznie się zmniejszyła. Każdy test ma tylko 3 linie kodu:
@Test fun `when all data is unavailable then return null`() { //ARRANGE mockBatteryProperties(DATA_NOT_AVAILABLE, DATA_NOT_AVAILABLE, DATA_NOT_AVAILABLE) //ACT val batteryCurrent = factory.getBatteryCurrent() //ASSERT assertNull(batteryCurrent) } @Test fun `should return null actualCurrent when is not available`() { //ARRANGE mockBatteryProperties(DATA_NOT_AVAILABLE, 222222222, 333333333) //ACT val batteryCurrent = factory.getBatteryCurrent() //ASSERT assertBatteryCurrent(batteryCurrent, null, 222.222222, 333.333333) } @Test fun `should return null averageCurrent when is not available`() { //ARRANGE mockBatteryProperties(111111111, DATA_NOT_AVAILABLE, 333333333) //ACT val batteryCurrent = factory.getBatteryCurrent() //ASSERT assertBatteryCurrent(batteryCurrent, 111.111111, null, 333.333333) } @Test fun `should return null actualCharge when is not available`() { //ARRANGE mockBatteryProperties(111111111, 222222222, DATA_NOT_AVAILABLE) //ACT val batteryCurrent = factory.getBatteryCurrent() //ASSERT assertBatteryCurrent(batteryCurrent, 111.111111, 222.222222, null) }
Test 5: Wszystkie wartości są dostępne
Jeśli wszystkie dane są dostępne to getBatteryCurrent
zwróciBatteryCurrent
ze wszystkimi danymi
@Test fun `factory should deliver battery current in amperes`() { //ARRANGE mockBatteryProperties(111111111, 222222222, 333333333) //ACT val batteryCurrent = factory.getBatteryCurrent() //ASSERT assertBatteryCurrent(batteryCurrent, 111.111111, 222.222222, 333.333333) }

Podsumowanie przykładu
Biblioteki do mockowania niesamowicie upraszczają testowanie zewnętrznego kodu, który nie posiada interfejsów. Trzeba jednak rozsądnie ich używać, ponieważ możesz dojść do sytuacji, w której klasy z logiką biznesową będą zawierały referencje do klas z Android SDK i tym samym naruszały zasadę DIP (Dependency Inversion Principle). Stosuj biblioteki do mockowania tam gdzie rzeczywiście nie da się zrobić własnego mocka ze względu na brak interfejsów w API.
Repozytorium
Kod źródłowy wraz z testami znajdziesz na repozytorium git na GitHub: https://github.com/androidCoder-pl/android-basicUnitTest-part1
Bibliografia
[1] http://blog.cleancoder.com/
[2] https://cleancoders.com/videos?series=clean-code&subseries=advanced-tdd
[3] Test Driven Devlopment: By Example
[4] https://developer.android.com/training/testing/fundamentals
[5] https://developer.android.com/training/testing/unit-testing
Super artykuł, wcześniej unikałem pisania testów ale teraz się to zmieni 😀 dzięki!
Dziękuję Ci za komentarz 😉 Jakbyś miał jakieś pytania dotyczące testowania to śmiało możesz pisać. Niedługo pojawią się następne artykuły zgłębiające temat pisania testów jednostkowych. Postaram się nie zawieść. Jeśli chciałbyś o czymś konkretnym przeczytać na blogu, to chętnie posłucham opinii.