Testing Patterns¶
Best practices for using Fakt-generated fakes in your test suites.
Isolated Fakes Per Test¶
Create fresh fakes for each test to avoid shared state:
class UserServiceTest {
@Test
fun `test case 1`() {
val fake = fakeRepository { // Fresh fake
getUser { id -> User(id, "Alice") }
}
// Test with Alice
}
@Test
fun `test case 2`() {
val fake = fakeRepository { // Fresh fake
getUser { id -> User(id, "Bob") }
}
// Test with Bob
}
}
Why this matters:
- Prevents test pollution (one test affecting another)
- Makes tests order-independent
- Easier to understand test setup
Configure Only What You Need¶
Don’t configure methods you don’t use in the test:
@Test
fun `GIVEN repository WHEN getting user THEN returns user`() {
val fake = fakeRepository {
getUser { id -> User(id, "Alice") }
// Don't configure saveUser, deleteUser, etc. if not used
}
val user = fake.getUser("123")
assertEquals("Alice", user.name)
}
Benefits:
- Tests are easier to read (only relevant setup visible)
- Reduces noise in test code
- Smart defaults handle unconfigured methods
Verify Behavior, Not Implementation¶
Use call counters to verify interactions:
@Test
fun `GIVEN service WHEN processing user THEN calls repository once`() {
val fakeRepo = fakeRepository()
val service = UserService(fakeRepo)
service.processUser("123")
assertEquals(1, fakeRepo.getUserCallCount.value)
assertEquals(1, fakeRepo.saveUserCallCount.value)
}
State-based verification example:
@Test
fun `GIVEN repository WHEN saving users THEN all users are saved`() {
val savedUsers = mutableListOf<User>()
val fake = fakeRepository {
saveUser { user ->
savedUsers.add(user)
Result.success(Unit)
}
}
val service = UserService(fake)
service.batchSave(listOf(alice, bob))
// Verify OUTCOME (state), not method calls
assertEquals(2, savedUsers.size)
assertTrue(savedUsers.contains(alice))
assertTrue(savedUsers.contains(bob))
}
Test Edge Cases¶
Configure fakes to test error handling:
@Test
fun `GIVEN repository failure WHEN saving user THEN handles error`() = runTest {
val fake = fakeRepository {
saveUser { user ->
Result.failure(NetworkException())
}
}
val service = UserService(fake)
val result = service.createUser("Alice")
assertTrue(result.isFailure)
}
Use Turbine for Reactive Testing¶
Test StateFlow call counters reactively:
@Test
fun `GIVEN fake WHEN calling repeatedly THEN emits counts`() = runTest {
val fake = fakeAnalytics()
fake.trackCallCount.test {
assertEquals(0, awaitItem())
fake.track("event1")
assertEquals(1, awaitItem())
fake.track("event2")
assertEquals(2, awaitItem())
}
}
Anti-Patterns to Avoid¶
❌ Shared Fakes Across Tests¶
// ❌ BAD: Shared fake
class UserServiceTest {
private val sharedFake = fakeRepository() // State leaks between tests
@Test
fun test1() { /* uses sharedFake */ }
@Test
fun test2() { /* uses same sharedFake - FLAKY! */ }
}
// ✅ GOOD: Fresh fake per test
class UserServiceTest {
@Test
fun test1() {
val fake = fakeRepository() // Isolated
}
@Test
fun test2() {
val fake = fakeRepository() // Independent
}
}
❌ Over-Configuring Fakes¶
// ❌ BAD: Configuring unused methods
@Test
fun `test getUser only`() {
val fake = fakeRepository {
getUser { id -> User(id, "Alice") }
saveUser { /* not used in this test */ }
deleteUser { /* not used in this test */ }
listUsers { /* not used in this test */ }
}
val user = fake.getUser("123") // Only this is tested
}
// ✅ GOOD: Configure only what's needed
@Test
fun `test getUser only`() {
val fake = fakeRepository {
getUser { id -> User(id, "Alice") }
// Smart defaults handle the rest
}
val user = fake.getUser("123")
}
Next Steps¶
- Migration Guide - From MockK/Mockito to Fakt
- Performance - Build time impact and optimization
- Usage Guide - Core patterns and examples