Mock — to obiekt zastępczy, który imituje zachowanie rzeczywistego komponentu i umożliwia weryfikację interakcji z nim. W przeciwieństwie do Stub, który po prostu zwraca zadaną wartość, Mock rejestruje fakt wywołania metody, przekazane argumenty i liczbę wywołań. Według danych Mockito (2024), Mock to najpopularniejszy typ Test Double w projektach Java i Kotlin, używany w ponad 70% testów jednostkowych aplikacji mobilnych.
Najważniejsze
Mock — to obiekt tworzony przez framework mockingowy (Mockito, MockK, EasyMock), który imituje interfejs lub klasę i rejestruje wszystkie wywołania swoich metod. Programista określa oczekiwania: metoda X zostanie wywołana z argumentami Y i zwróci Z. Po wykonaniu testu Mock sprawdza, czy oczekiwania są zgodne z rzeczywistymi wywołaniami.
Termin pochodzi z teatralnej metafory Test Doubles: Mock to „imitator“, który nie tylko stoi na scenie (jak Dummy), ale odgrywa rolę i weryfikuje, czy interakcja z nim była prawidłowa. Jeśli testowany kod nie wywołał metody, której Mock oczekiwał, lub wywołał ją z nieprawidłowymi argumentami — test kończy się niepowodzeniem z komunikatem o naruszonym oczekiwaniu.
Mock jest tworzony przez fabrykę frameworka: mockk<MyInterface>() lub Mockito.mock(MyClass.java). Framework generuje obiekt proxy, który przechwytuje wszystkie wywołania metod. Każde wywołanie jest porównywane z wcześniej zdefiniowanymi oczekiwaniami (expectations). Jeśli wywołanie odpowiada oczekiwaniu — zwracana jest zadana wartość. Jeśli nie — Mock zwraca wartość domyślną lub zgłasza wyjątek, w zależności od konfiguracji.
Mock jest obowiązkowy, gdy testowany kod współdziała z komponentami mającymi efekty uboczne: wysyłanie danych na serwer, zapis do bazy danych, logowanie, analityka, nawigacja, wyświetlanie okien systemowych. Bez Mocka te interakcje są niemożliwe do zweryfikowania bez uruchamiania rzeczywistej infrastruktury. Według Google Testing Blog, Mock to jedyny sposób, aby sprawdzić, czy aplikacja rzeczywiście wysłała zdarzenie analityczne bez uruchamiania serwera testowego.
Różnica między Mock a Stub to jeden z najczęściej dyskutowanych tematów w testowaniu. Oba typy zastępują rzeczywistą zależność, ale w zasadniczo różny sposób.
| Kryterium | Mock | Stub |
|---|---|---|
| Główne pytanie | Czy metoda została wywołana? | Jaki wynik został zwrócony? |
| Weryfikacja | Zachowania (verify) | Stanu (assert) |
| Zwrot danych | Opcjonalnie | Obowiązkowo |
| Przykład | verify(analytics).logEvent("click") | assertEquals(5, repository.getCount()) |
| Kiedy stosować | Efekty uboczne | Zwrot danych |
Prosty test wyboru: zadaj sobie pytanie — „jeśli usunę tę linię kodu, czy test upadnie?“. Jeśli test weryfikuje wartość zwracaną — potrzebny jest Stub (weryfikacja przez assert). Jeśli test sprawdza, czy kod wywołał metodę z prawidłowymi argumentami — potrzebny jest Mock (weryfikacja przez verify). Ta dychotomia wynika ze wzorca Command-Query Separation: metody zmieniające stan (commands) wymagają Mocka; metody zwracające dane (queries) wymagają Stuba.
Wybór między Mockito a MockK to jedna z pierwszych decyzji przy konfiguracji stosu testowego projektu Android w Kotlin. Obie biblioteki wykonują to samo zadanie, ale różnią się podejściem do specyfiki Kotlin.
Mockito — de facto standard dla projektów Java. Wersja 5.x obsługuje obiekty mock dla klas finalnych, metod statycznych i konstruktorów dzięki wbudowanemu MockMaker. Dla projektów Kotlin Mockito wymaga dodatkowej konfiguracji: rozszerzenia mockito-kotlin dla lepszej składni, mockito-inline dla klas finalnych. Mockito nie obsługuje korutyn Kotlin i funkcji suspend bez dodatkowych adapterów.
MockK został stworzony specjalnie dla Kotlin. Natywnie obsługuje korutyny (coEvery, coVerify), sealed class, data class, singleton-y object i funkcje rozszerzające. Składnia MockK wykorzystuje DSL z blokami lambda, co wygląda naturalnie w kodzie Kotlin. MockK potrafi także mockować właściwości (property mocking) bez dodatkowej konfiguracji — to ważne dla projektów Android używających LiveData, StateFlow i Delegates.
// Mockito + mockito-kotlin
val repository = mock<UserRepository>()
whenever(repository.getUser(1)).thenReturn(user)
// MockK
val repository = mockk<UserRepository>()
every { repository.getUser(1) } returns user
Benchmarki (JVM Benchmark, 2024) pokazują, że MockK tworzy obiekty mock o 15–20% szybciej niż Mockito dla projektów Kotlin dzięki bezpośredniej pracy z bytecodem Kotlin, a nie Java Reflections. Dla projektów z tysiącami testów jednostkowych różnica w czasie budowania może być zauważalna: MockK oszczędza 30–60 sekund przy pełnym przebiegu testów w dużych projektach.
Rozpatrzymy trzy scenariusze: testowanie ViewModel z zależnościami Mock, testowanie UseCase z weryfikacją wywołania API i testowanie korutyn z coVerify.
class ProfileViewModelTest {
private val analytics = mockk<AnalyticsService>()
private val repo = mockk<UserRepository>()
private val vm = ProfileViewModel(repo, analytics)
fun `profile opened logs analytics event`() {
every { analytics.logEvent("profile_opened") } returns Unit
vm.onViewCreated()
verify { analytics.logEvent("profile_opened") }
}
}
class SendMessageUseCaseTest {
private val api = mockk<MessagingApi>()
private val useCase = SendMessageUseCase(api)
fun `send message with correct payload`() = runTest {
val message = Message(text = "Hello", userId = 42)
coEvery { api.sendMessage(any()) } returns MessageResult.Sent("msg_1")
val result = useCase.execute(message)
coVerify {
api.sendMessage(match {
it.text == "Hello" && it.userId == 42
})
}
assertTrue(result is MessageResult.Sent)
}
}
class OrderUseCaseTest {
private val api = mockk<OrderApi>()
private val useCase = OrderUseCase(api)
private val slot = slot<OrderRequest>()
fun `order request contains correct items`() = runTest {
coEvery { api.placeOrder(capture(slot)) } returns OrderResult.Placed("order_1")
useCase.execute(listOf("item_a", "item_b"))
assertEquals(2, slot.captured.items.size)
assertEquals("item_a", slot.captured.items[0])
}
}
Efektywne wykorzystanie Mock w programowaniu mobilnym wymaga przestrzegania dyscypliny. Naruszenie tych zasad zamienia testy w kruche przeszkody, które psują się przy każdym refaktoringu.
Sztywna zasada: Mock jest tworzony tylko dla zależności przekraczających granicę aplikacji: klienty API, bazy danych, system plików, usługi systemowe (LocationManager, BluetoothAdapter, Camera). Wewnętrzne klasy aplikacji — encje domenowe, Value Object, proste narzędzia — nie powinny być zastępowane przez Mock. Ich zachowanie testuje się przez rzeczywiste obiekty.
Każdy test powinien zawierać dokładnie jedną logiczną kontrolę — albo verify (dla Mocka), albo assert (dla Stuba). Nie mieszaj weryfikacji stanu i zachowania w jednym teście. Jeśli trzeba sprawdzić zarówno wywołanie API, jak i wynik — utwórz dwa osobne testy z różnymi nazwami. Ta zasada, znana jako „jeden assert na test“, wywodzi się z zaleceń Kenta Becka (2002).
Oprócz podstawowego mockowania istnieją zaawansowane techniki rozwiązujące specyficzne problemy w programowaniu mobilnym: testowanie wielowątkowości, weryfikacja stanu Flow i częściowe mockowanie rzeczywistych obiektów.
Spy (lub partial mock) umożliwia utworzenie obiektu, który deleguje wywołania do rzeczywistej implementacji, ale pozwala nadpisać poszczególne metody. W MockK spyk jest tworzony na podstawie rzeczywistej instancji klasy: val repo = spyk(InMemoryUserRepository()). Wywołania, dla których zdefiniowano oczekiwania przez every, przechodzą przez Mock; pozostałe — przez rzeczywisty obiekt. Spy jest szczególnie przydatny do testowania legacy-kodu, gdzie wstrzykiwanie zależności nie zostało jeszcze wprowadzone, a trzeba nadpisać tylko jedną metodę.
W nowoczesnych projektach Android na Jetpack Compose ViewModel eksponuje stan przez StateFlow. MockK umożliwia mockowanie zależności Flow, a biblioteka Turbine upraszcza weryfikację emisji. Klasyczny wzorzec: MockK dla UseCase zwracającego Flow, Turbine do weryfikacji emisji ViewModel. Ten stos jest zalecany przez dokumentację Android Testing (Google, 2024) dla projektów na Kotlin Coroutines.
class SearchViewModelTest {
private val searchUseCase = mockk<SearchUseCase>()
private val vm = SearchViewModel(searchUseCase)
fun `search emits results`() = runTest {
coEvery { searchUseCase.search("android") } returns
flowOf(SearchResult.Success(listOf(Item("Android TDD"))))
vm.search("android")
vm.state.test {
val state = awaitItem()
assertTrue(state.items.isNotEmpty())
cancelAndIgnoreRemainingEvents()
}
}
}
Często zadawane pytania
Mock — to koncepcja, typ Test Double weryfikujący zachowanie. Mockito — to biblioteka do tworzenia obiektów Mock w Java i Android. Inne biblioteki: MockK (Kotlin), EasyMock (Java), Cuckoo (iOS).
Do testowania funkcji suspend z Mock używaj MockK (coEvery / coVerify) lub Mockito z mockito-kotlin. MockK obsługuje korutyny natywnie: coEvery określa zachowanie funkcji suspend, coVerify weryfikuje jej wywołanie wewnątrz korutyny. Wszystkie wywołania suspend muszą być wykonywane wewnątrz runTest (kotlinx-coroutines-test).
Tak. W MockK służy do tego returnsMany: every { api.getData() } returnsMany listOf(response1, response2). W Mockito — łańcuch thenReturn(value1).thenReturn(value2). Jest to przydatne do testowania zachowania przy sekwencyjnych wywołaniach z różnymi odpowiedziami.
W MockK używaj adnotacji @MockK z polem relaxed = true i wywołuj clearMocks(mock) w metodzie @After. W Mockito — Mockito.reset(mock). Najlepszą praktyką jest tworzenie nowego Mocka dla każdego testu przez @Before, aby wykluczyć wpływ między testami.
MockK poprawnie współpracuje z sealed class: every { useCase() } returns Result.Success(data). Mockito nie obsługuje sealed class bezpośrednio, wymagając obejść. To jeden z powodów, dla których w projektach Kotlin zaleca się MockK zamiast Mockito.
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ż