Test Doubles — to obiekty zastępcze używane w testach jednostkowych zamiast rzeczywistych zależności. Termin został wprowadzony przez Gerarda Meszarosa w książce „xUnit Test Patterns“ (2007) jako pojęcie ogólne dla Mock, Stub, Fake, Spy i Dummy. Według Martina Fowlera (2024), Test Doubles pozwalają na izolację testowanego komponentu od jego otoczenia, czyniąc testy deterministycznymi, szybkimi i niezależnymi od zewnętrznych serwisów.
Najważniejsze
Test Doubles — to termin pochodzący z przemysłu motoryzacyjnego (kaskader, „double“ dla aktorów), przeniesiony do tworzenia oprogramowania. Jak kaskader zastępuje aktora w niebezpiecznej scenie, Test Double zastępuje rzeczywisty komponent w scenariuszu testowym. Jest to konieczne, gdy rzeczywista zależność jest niedostępna, wolna, niedeterministyczna lub ma skutki uboczne.
Pojęcie Test Double obejmuje pięć konkretnych typów, z których każdy rozwiązuje swoje zadanie. Typologia Meszarosa jest kanoniczna i używana we wszystkich nowoczesnych podręcznikach testowania. Różnica między typami polega na stopniu kontroli i weryfikacji: od prostego wypełniania parametrów (Dummy) do pełnego sprawdzenia sekwencji wywołań (Mock).
Głównym celem Test Doubles jest izolacja testowanego modułu. W programowaniu mobilnym rzeczywiste zależności to serwery API, bazy danych, system plików, czujniki urządzenia, usługi systemowe (LocationManager, Camera, Bluetooth). Bezpośrednie używanie tych komponentów sprawia, że testy są wolne, kruche i zależne od otoczenia. Według Google Testing Blog (2023), dobrze izolowane testy jednostkowe wykonują się w ciągu milisekund, a integracyjne — w sekundach i minutach.
Klasyfikacja Gerarda Meszarosa obejmuje pięć typów Test Doubles, różniących się zachowaniem i celem użycia. Zrozumienie różnic między nimi to podstawa poprawnego testowania jednostkowego.
Dummy — to obiekt przekazywany do testowanej metody, ale nigdy nie używany. Dummy jest potrzebny tylko do spełnienia sygnatury metody. W Kotlin jest to często null, emptyList() lub obiekt z atrapami. Dummy nie powinien zawierać żadnej logiki — jeśli zostanie wywołany, test powinien zakończyć się niepowodzeniem.
Fake — to uproszczona, ale działająca implementacja interfejsu. W przeciwieństwie do Mock i Stub, Fake zawiera rzeczywistą logikę biznesową, ale w uproszczonej formie. Klasyczny przykład — InMemoryUserRepository, który przechowuje dane w HashMap zamiast w bazie danych. Fake jest używany, gdy trzeba przetestować logikę zależną od stanu, ale bez narzutu rzeczywistej infrastruktury.
| Typ | Przeznaczenie | Przykład |
|---|---|---|
| Dummy | Wypełnić parametr | null, pusty obiekt |
| Fake | Działająca uproszczona implementacja | InMemoryRepository |
| Stub | Zwrócić stałą wartość | when(api.getUser()).thenReturn(user) |
| Spy | Zarejestrować wywołania do sprawdzenia | verify(spy).save(user) |
| Mock | Sprawdzić interakcję | verify(mock).sendEmail(email) |
Stub zwraca z góry określone wartości na konkretne wywołania. Stub nie sprawdza, czy został wywołany — po prostu dostarcza dane. W Mockito Stub tworzy się przez when(method).thenReturn(value). Stub jest idealny do testowania, gdy zależność ma zwrócić konkretną wartość, ale sam fakt wywołania nie jest istotny.
Spy — to otoczka wokół rzeczywistego obiektu, która rejestruje wszystkie wywołania do późniejszej weryfikacji. W przeciwieństwie do Mock, Spy deleguje wywołania do rzeczywistego obiektu, ale pozwala sprawdzić, że one nastąpiły. W Mockito Spy tworzy się przez spy(realObject). Spy jest przydatny do częściowego mockowania, gdy chce się używać rzeczywistego obiektu, ale sprawdzić niektóre wywołania.
Mock — to obiekt z predefiniowanymi oczekiwaniami wywołań. Mock sprawdza, czy określone metody zostały wywołane z określonymi argumentami i w określonej kolejności. W przeciwieństwie do Stub, Mock koncentruje się na weryfikacji zachowania, a nie na zwracaniu danych. Mock — to najpotężniejszy i najczęściej używany typ Test Double w programowaniu mobilnym.
Różnica między Mock a Stub często powoduje zamieszanie nawet u doświadczonych programistów. Główna różnica polega na celu: Stub sprawdza stan (state verification), Mock sprawdza zachowanie (behavior verification).
Stub odpowiada na pytanie: „czy kod zwrócił poprawny wynik?“. Mock odpowiada na pytanie: „czy kod wywołał odpowiednie metody z odpowiednimi argumentami?“. W programowaniu mobilnym Stub jest używany, gdy ważny jest rezultat (np. dane z repozytorium), a Mock — gdy ważne są efekty uboczne (np. wysłanie emaila, zapis do bazy danych).
// Stub: sprawdzanie stanu
every { repository.getUsers() } returns listOf(user)
val result = useCase.getUsers()
assertEquals(1, result.size)
// Mock: sprawdzanie zachowania
every { analytics.logEvent("purchase") } returns Unit
useCase.purchase(item)
verify { analytics.logEvent("purchase") }
Praktyczne przykłady wszystkich pięciu typów Test Doubles w Kotlin z użyciem MockK — najpopularniejszej biblioteki mocking dla projektów Android.
class InMemoryUserRepository : UserRepository {
private val store = mutableMapOf<String, User>()
override fun save(user: User) {
store[user.email] = user
}
override fun findByEmail(email: String): User? {
return store[email]
}
}
class RegisterUseCaseTest {
private val api = mockk<AuthApi>()
private val repo = spyk(InMemoryUserRepository())
private val useCase = RegisterUseCase(api, repo)
fun `register user successfully`() = runTest {
// Stub: zwracamy stałą odpowiedź API
coEvery { api.register("test@test.com") } returns AuthResult.Success("token123")
val result = useCase.execute("test@test.com")
// Verify: sprawdzamy, czy użytkownik został zapisany
verify { repo.save(any()) }
assertTrue(result.isSuccess())
}
}
data class Logger(val appContext: Context, val format: FormatType)
fun `test logger with dummy context`() {
// Dummy: Context nie jest używany wewnątrz Logger
val dummyContext = mockk<Context>()
val logger = Logger(dummyContext, FormatType.JSON)
assertEquals(FormatType.JSON, logger.format)
}
Wybór typu Test Double zależy od tego, co dokładnie jest testowane: stan, zachowanie czy integracja. W programowaniu mobilnym na Android i iOS powstały następujące zalecenia.
Przy testowaniu ViewModel używaj Mock dla zależności, które powodują efekty uboczne (repozytoria, analityka, nawigacja), oraz Stub dla zależności, które zwracają dane (klienty API, ContentProvider). Pozwala to sprawdzić, czy ViewModel poprawnie obsługuje zarówno udane, jak i błędne scenariusze.
Na poziomie Repository preferowane są Fake (implementacje bazy danych in-memory) i Stub (stałe odpowiedzi API). Fake pozwala sprawdzić logikę buforowania i trybu offline bez konfiguracji SQLite. Stub symuluje różne kody HTTP: 200, 404, 500, timeout.
Nieprawidłowe używanie Test Doubles — jedna z najczęstszych przyczyn kruchych testów, które psują się przy każdej refaktoryzacji.
Najczęstszy błąd — mockowanie wszystkiego. Jeśli każda zależność w teście jest zastąpiona przez Mock, test przestaje sprawdzać rzeczywiste zachowanie. Mock powinien być tylko dla zewnętrznych zależności (sieć, baza danych, system plików, usługi systemowe). Wewnętrzne komponenty aplikacji (Value Object, data class, proste narzędzia) nie powinny być zastępowane.
Drugi błąd — tworzenie Mock bez określenia oczekiwań. Jeśli metoda jest wywoływana bez every / when, Mock zwraca wartość domyślną (null, 0, false). Może to prowadzić do fałszywie pozytywnych testów, gdy Mock milcząco zwraca null, a test interpretuje to jako poprawne zachowanie.
Trzeci błąd — sprawdzanie każdego wywołania każdego Mocka. Verify powinien być używany tylko dla wywołań krytycznych z punktu widzenia logiki biznesowej. Nadmierna weryfikacja czyni testy kruchymi: zmiana kolejności wywołań w kodzie produkcyjnym psuje testy bez zmiany zachowania.
Często zadawane pytania
Stub zwraca dane i sprawdza stan (co zostało zwrócone), a Mock sprawdza zachowanie (jakie metody zostały wywołane). Stub = „zwróć X“, Mock = „sprawdź, czy wywołano Y z argumentem Z“. W rzeczywistych testach jeden obiekt często pełni rolę zarówno Stub, jak i Mock jednocześnie.
Fake jest preferowany nad Mock, gdy testowana jest logika zależna od stanu: buforowanie, tryb offline, transakcje. Fake (implementacja in-memory) pozwala sprawdzić te scenariusze bez kruchych wywołań verify. Mock lepiej nadaje się do sprawdzania wysyłania danych: analityka, powiadomienia push, email.
Dla projektów Android w Kotlin zalecana jest MockK. Obsługuje korutyny, suspend-funkcje, sealed class i extension-funkcje bez dodatkowej konfiguracji. Dla projektów w Javie nadal standardem jest Mockito — najpopularniejsza biblioteka z obszerną dokumentacją.
Do testowania Kotlin Flow używaj biblioteki Turbine w parze z MockK. Turbine upraszcza sprawdzanie emisji Flow: można sprawdzić kolejność wartości, zakończenie strumienia i wyjątki. Stub dla Flow zwraca flowOf(value), Mock sprawdza, czy Flow został zebrany.
Tak, ale na poziomie odpowiedzi API, a nie komponentów UI. Biblioteki MockWebServer (OkHttp) i WireMock pozwalają podmieniać odpowiedzi HTTP w testach UI. Same komponenty UI (Compose, SwiftUI Views) nie powinny być zastępowane — ich zachowanie testuje się przez screenshot-test i Espresso.
Podsumowanie
Opracujemy aplikację mobilną pod klucz
IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.
Przeczytaj również