Migration from Mocks¶
Migrating from mocking frameworks (MockK, Mockito, Mokkery, Mockative) to Fakt compile-time fakes.
Why Migrate?¶
- KMP compatibility - Mocks rely on JVM reflection; fakes work everywhere
- Refactor-safe tests - Verify outcomes, not implementation details
- Zero runtime cost - Compile-time generation, no reflection overhead
- Simple API - DSL lambdas match your interface signatures
MockK → Fakt¶
Basic Usage¶
MockK:
@Test
fun `GIVEN mock repository WHEN processing user THEN calls getUser`() {
val mock = mockk<UserRepository>()
every { mock.getUser(any()) } returns User("123", "Alice")
val service = UserService(mock)
service.processUser("123")
verify(exactly = 1) { mock.getUser("123") }
}
Fakt:
@Test
fun `GIVEN fake repository WHEN processing user THEN calls getUser`() {
val fake = fakeUserRepository {
getUser { id -> User(id, "Alice") }
}
val service = UserService(fake)
service.processUser("123")
assertEquals(1, fake.getUserCalls.value.size)
}
Suspend Functions with Error Handling¶
MockK:
@Test
fun `GIVEN repository failure WHEN fetching data THEN handles error`() = runTest {
val mock = mockk<DataService>()
coEvery { mock.fetchData() } throws NetworkException("Connection timeout")
val viewModel = DataViewModel(mock)
viewModel.loadData()
assertTrue(viewModel.errorState.value is Error.Network)
}
Fakt:
@Test
fun `GIVEN repository failure WHEN fetching data THEN handles error`() = runTest {
val fake = fakeDataService {
fetchData { throw NetworkException("Connection timeout") }
}
val viewModel = DataViewModel(fake)
viewModel.loadData()
assertTrue(viewModel.errorState.value is Error.Network)
}
Key difference: Fakt’s DSL lambda has the same signature as the original interface method. No need to learn separate stubbing APIs (coEvery, every, returns).
Mockito → Fakt¶
Mockito:
@Test
fun `GIVEN repository WHEN saving user THEN returns success`() {
val mock = mock(UserRepository::class.java)
`when`(mock.saveUser(any())).thenReturn(Result.success(Unit))
val service = UserService(mock)
val result = service.createUser("Alice")
assertTrue(result.isSuccess)
verify(mock, times(1)).saveUser(any())
}
Fakt:
@Test
fun `GIVEN repository WHEN saving user THEN returns success`() = runTest {
val fake = fakeUserRepository {
saveUser { user -> Result.success(Unit) }
}
val service = UserService(fake)
val result = service.createUser("Alice")
assertTrue(result.isSuccess)
assertEquals(1, fake.saveUserCalls.value.size)
}
Mokkery → Fakt¶
Mokkery:
@Test
fun `GIVEN mock analytics WHEN tracking event THEN records event`() = runTest {
val mock = mock<Analytics>()
everySuspend { mock.track(any()) } returns Unit
val service = AnalyticsService(mock)
service.logUserAction("button_click")
verifySuspend { mock.track("button_click") }
}
Fakt:
@Test
fun `GIVEN fake analytics WHEN tracking event THEN records event`() = runTest {
val trackedEvents = mutableListOf<String>()
val fake = fakeAnalytics {
track { event -> trackedEvents.add(event) }
}
val service = AnalyticsService(fake)
service.logUserAction("button_click")
assertEquals(1, fake.trackCalls.value.size)
assertEquals("button_click", trackedEvents.first())
}
Migration notes:
- Replace
mock<T>()withfakeT {} - Replace
everySuspend { }with DSL lambda configuration - Replace
verifySuspend { }with StateFlow call history - State-based verification (checking
trackedEvents) is more resilient than interaction verification
Mockative → Fakt¶
Mockative:
@Test
fun `GIVEN repository WHEN fetching user THEN returns user`() = runTest {
val mock = mock<UserRepository>()
given(mock).coroutine { getUser("123") }.thenReturn(User("123", "Alice"))
val viewModel = UserViewModel(mock)
viewModel.loadUser("123")
assertEquals("Alice", viewModel.userName.value)
}
Fakt:
@Test
fun `GIVEN repository WHEN fetching user THEN returns user`() = runTest {
val fake = fakeUserRepository {
getUser { id -> User(id, "Alice") }
}
val viewModel = UserViewModel(fake)
viewModel.loadUser("123")
assertEquals("Alice", viewModel.userName.value)
assertEquals(1, fake.getUserCalls.value.size)
}
Generic Repositories¶
MockK:
@Test
fun `GIVEN generic repository WHEN saving item THEN returns success`() {
val mock = mockk<Repository<User>>()
every { mock.save(any()) } returns Result.success(Unit)
val service = CrudService(mock)
service.createUser(User("123", "Alice"))
verify { mock.save(any<User>()) }
}
Fakt:
@Test
fun `GIVEN generic repository WHEN saving item THEN returns success`() {
val fake = fakeRepository<User> {
save { item -> Result.success(Unit) }
}
val service = CrudService(fake)
service.createUser(User("123", "Alice"))
assertEquals(1, fake.saveCalls.value.size)
}
Type safety: Fakt preserves generic type parameters. The DSL lambda receives item: User, not item: Any.
Verification Patterns¶
Fakt’s verification DSL provides expressive assertions comparable to mocking frameworks.
MockK verify → Fakt verify{Method}¶
MockK:
@Test
fun `verify specific arguments`() {
val mock = mockk<UserRepository>()
every { mock.save(any(), any()) } returns Unit
mock.save(User("1", "Alice"), true)
mock.save(User("2", "Bob"), false)
verify(exactly = 2) { mock.save(any(), any()) }
verify { mock.save(User("1", "Alice"), true) }
}
Fakt:
@Test
fun `verify specific arguments`() {
val fake = fakeUserRepository {
save { user, validate -> }
}
fake.save(User("1", "Alice"), true)
fake.save(User("2", "Bob"), false)
fake.verifySave {
assertTrue(wasCalledTimes(2))
assertTrue(wasCalledWith(User("1", "Alice"), true))
}
}
Mockito verify → Fakt verification¶
Mockito:
@Test
fun `verify call order and counts`() {
val mock = mock(Analytics::class.java)
mock.track("page_view")
mock.track("purchase")
verify(mock, times(2)).track(any())
verify(mock).track("page_view")
verify(mock, never()).track("error")
}
Fakt:
@Test
fun `verify call order and counts`() {
val fake = fakeAnalytics()
fake.track("page_view")
fake.track("purchase")
fake.verifyTrack {
assertTrue(wasCalledTimes(2))
assertTrue(wasCalledWith("page_view"))
assertTrue(neverCalledWith("error"))
assertTrue(wasCalledInOrder("page_view", "purchase"))
}
}
ArgumentCaptor → first, lastOrNull, all¶
MockK with slot:
@Test
fun `capture and inspect arguments`() {
val mock = mockk<UserRepository>()
val slot = slot<User>()
every { mock.save(capture(slot), any()) } returns Unit
mock.save(User("1", "Alice"), true)
assertEquals("Alice", slot.captured.name)
}
Mockito with ArgumentCaptor:
@Test
fun `capture and inspect arguments`() {
val mock = mock(UserRepository::class.java)
val captor = ArgumentCaptor.forClass(User::class.java)
mock.save(User("1", "Alice"), true)
verify(mock).save(captor.capture(), any())
assertEquals("Alice", captor.value.name)
}
Fakt with call history:
@Test
fun `capture and inspect arguments`() {
val fake = fakeUserRepository {
save { user, validate -> }
}
fake.save(User("1", "Alice"), true)
fake.save(User("2", "Bob"), false)
fake.verifySave {
// Access first call
assertEquals("Alice", first.user.name)
assertTrue(first.validate)
// Access last call
assertEquals("Bob", lastOrNull?.user?.name)
// Access all calls
assertEquals(2, all.size)
assertEquals(listOf("Alice", "Bob"), all.map { it.user.name })
}
}
Key differences:
- No need for separate captor/slot objects
- Type-safe access to all parameters via generated data classes
first,lastOrNull, andallprovide natural history access- Call history persists for the lifetime of the fake
Next Steps¶
- Testing Patterns - Best practices for fake-based testing
- Usage Guide - Core Fakt patterns and examples
- Performance - Build time impact and optimization