| name | mocking-strategies |
| description | Guidance on choosing between fakes, stubs, and mocking libraries (MockK, Mockito, mocktail, ts-jest, Swift test doubles) for mobile unit tests. Use when asked to mock a collaborator or to decide which double to use. |
Mocking Strategies for Mobile
Instructions
Most "mocking" problems are really design problems: the code under test depends on a concrete implementation instead of an interface, or on a framework class it does not own. Fix the seam first; pick the double second.
1. Hierarchy of Test Doubles
From most preferred to least:
- Fake — a working in-memory implementation of an interface (e.g.,
InMemoryUserRepository). Durable, readable, reusable across tests.
- Stub — returns hard-coded answers to method calls, no behavior.
- Spy — a real object with selected method interactions recorded.
- Mock — a test-only object whose interactions are asserted.
Default to fakes for stateful collaborators (repositories, caches, stores) and stubs/mocks only for thin boundary adapters (loggers, analytics, network clients).
2. Do Not Mock What You Do Not Own
Never mock OkHttpClient, URLSession, Dio, fetch, Firebase*, CoreLocation, etc. directly. Wrap them in a seam you own:
interface WeatherApi { suspend fun forecast(city: String): Forecast }
class OkHttpWeatherApi(private val client: OkHttpClient) : WeatherApi { }
Then fake or mock WeatherApi in tests. The wrapper is tested once as an integration test.
3. Library Choices
Kotlin — MockK (preferred) for idiomatic Kotlin including coroutines and top-level functions:
val repo = mockk<UserRepository>()
coEvery { repo.load(id = 42) } returns User("Ada")
Java — Mockito / Mockito-Kotlin:
UserRepository repo = mock(UserRepository.class);
when(repo.load(42)).thenReturn(new User("Ada"));
Swift — manual protocol doubles (preferred):
final class UserRepositoryFake: UserRepository {
var users: [Int: User] = [:]
func load(id: Int) async throws -> User {
guard let u = users[id] else { throw UserError.notFound }
return u
}
}
Use Cuckoo or Mockingbird only when the codebase is already committed to them; hand-written doubles usually win on readability.
Dart — mocktail (null-safe, no codegen):
class MockUserRepo extends Mock implements UserRepository {}
final repo = MockUserRepo();
when(() => repo.load(42)).thenAnswer((_) async => User('Ada'));
Prefer mocktail over mockito in new Flutter code — no build_runner step.
TypeScript / Jest:
const repo: jest.Mocked<UserRepository> = {
load: jest.fn().mockResolvedValue({ name: 'Ada' }),
};
For module-level mocks prefer jest.mock('./module', factory) over auto-mocking; auto-mocks silently drift from the real module.
4. Verify vs Assert
Assert on outputs and observable state by default:
val result = sut.signIn(email, password)
result shouldBe Success(userId = 1)
Verify interactions only when the interaction is the behavior:
verify(exactly = 1) { analytics.track(SignInSucceeded(userId = 1)) }
Every verify you write is a line the next refactor might break without the behavior actually changing. Treat them as expensive.
5. Argument Matchers and Captors
Match only on what matters. any() everywhere hides bugs; over-specific matchers make tests brittle. When you need to inspect a complex argument, use a captor:
val slot = slot<AnalyticsEvent>()
verify { analytics.track(capture(slot)) }
slot.captured.name shouldBe "sign_in_succeeded"
6. Time, Randomness, IDs
These are collaborators. Inject them instead of mocking statics:
class OrderFactory(private val clock: Clock, private val ids: IdGenerator)
Then pass a FixedClock(Instant.parse("2026-01-01T00:00:00Z")) and SequentialIdGenerator() in tests.
7. Common Smells
- Mocking
Logger / println — usually unnecessary; if you must, assert on a collected log list, not on call counts.
- Mocking
DateTime.now() by patching a global — inject a clock instead.
- Five-deep
when { ... } chains — the SUT is doing too much; split it.
- Matching on
anyOrNull() for every argument — the test asserts nothing.
8. Checklist