Tools
Tools: Fakt: Automating the Fake-over-mock pattern
2026-02-25
0 views
admin
What Fakt Does ## The Testing Problem ## The Mock Tax ## The KMP Dead End ## Why Compiler Plugins Work ## Why Fakes Over Mocks ## Practical Guidance ## Fakes vs. Mocks: Quick Comparison ## Choosing the Right Tool ## Works Cited Kotlin testing has a problem that gets worse the more successful your project becomes. Manual test fakes don't scale—each interface requires 60-80 lines of boilerplate that silently drifts from reality during refactoring. Runtime mocking frameworks (MockK, Mockito) solve the boilerplate but introduce severe performance penalties and don't work on Kotlin/Native or WebAssembly. KSP-based tools promised compile-time generation, but Kotlin 2.0 broke them all. Fakt is a compiler plugin that generates production-quality fakes through deep integration with Kotlin's FIR and IR compilation phases—the same extension points used by Metro, a production DI framework from Zac Sweers. https://github.com/rsicarelli/fakt Fakt reduces fake boilerplate to an annotation: At compile time, Fakt generates a complete fake implementation. You use it through a type-safe factory: Consider a simple interface: A proper, production-quality fake requires ~40-60 lines of boilerplate: The problems: N methods require ~10N lines. Interface changes don't break unused fakes—they silently drift. For 50 interfaces, this means thousands of lines of brittle boilerplate. Runtime mocking frameworks solve the boilerplate but pay a different cost. Kotlin classes are final by default, so MockK and Mockito resort to bytecode instrumentation. Independent benchmarks1 quantify the penalty: A production test suite with 2,668 tests experienced a 2.7x slowdown (7.3s → 20.0s) when using mock-maker-inline3. For large projects, the mock tax accumulates to 40% slower test suites1. Runtime mocking relies on JVM-specific features: reflection, bytecode instrumentation, dynamic proxies. Kotlin/Native and Kotlin/Wasm compile to machine code. There is no JVM. MockK and Mockito cannot run in commonTest source sets targeting Native or Wasm45. The community attempted KSP-based solutions, but Kotlin 2.0's K2 compiler broke them. The StreetComplete app (10,000+ tests) was forced to migrate mid-project6. KSP-based tools (Mockative, MocKMP) operated at the symbol level—after type resolution, with limited access to the type system. When K2 landed, they broke. Compiler plugins operate during compilation, with full access to FIR and IR. They survive Kotlin version updates. Fakt uses a two-phase FIR → IR architecture: This is the same pattern used by Metro, Zac Sweers' DI compiler plugin. Metro's architecture has proven stable across Kotlin 1.9, 2.0, and 2.1. Beyond performance, fakes represent a different testing philosophy. Martin Fowler's "Mocks Aren't Stubs"7 describes two schools: state-based testing (verify outcomes) and interaction-based testing (verify method calls). The problem with interaction-based tests: they couple to implementation details8. Refactor a method signature without changing behavior, and mock-based tests break. Google's Testing Blog defines resilience as a critical test quality—"a test shouldn't fail if the code under test isn't defective"9. Mock-based tests often violate this. Google's "Now in Android" app makes this explicit10: "Don't use mocking frameworks. Instead, use fakes." The goal: "less brittle tests that may exercise more production code, instead of just verifying specific calls against mocks"11. Kotlin's async testing stack—runTest, TestDispatcher, Turbine12—is inherently state-based. Turbine's awaitItem() verifies emitted values, not method calls. The natural data source for this stack is a fake with MutableStateFlow backing. Fakt automates this pattern. Fakt and mocking libraries solve overlapping but distinct problems. Choosing between them depends on your constraints and testing needs. Fakt works best when: You've already chosen fakes over mocks. If you understand the state-based testing philosophy and prefer testing outcomes over verifying interactions, Fakt automates what you'd otherwise write by hand. You only use mocks for convenience. Many developers reach for mocking frameworks not for verify { } features, but simply because writing manual fakes is tedious. Fakt gives you the factory convenience without the mock overhead—generated fakes are plain Kotlin classes. You're building for Kotlin Multiplatform. Fakt generates plain Kotlin that compiles on JVM, Native, and WebAssembly—no reflection required. This applies to any source set, not just commonTest. You value exercising production code in tests. Fakt-generated fakes are real implementations your tests compile against, catching interface drift at build time rather than runtime. Tests run concurrently. Fakt tracks call history with StateFlow, which is thread-safe by design. Manual fakes with var count = 0 break under parallel execution. Mocking libraries (Mokkery, MockK) work best when: You need spy behavior. Partial mocking of real implementations—calling real methods while intercepting others—is something only mocking frameworks can do. Fakt generates new implementations, it doesn't wrap existing ones. You're mocking third-party classes without interfaces. If a library exposes final classes with no interface to program against, mocking frameworks can instrument the bytecode. Fakt requires an interface to annotate. Neither tool replaces contract testing. For third-party HTTP APIs, use WireMock or Pact. Hand-written fakes for external services drift from reality without contract validation—they create dangerous illusions of fidelity that break in production. Benchmarking Mockk — Avoid these patterns for fast unit tests. Kevin Block. https://medium.com/@_kevinb/benchmarking-mockk-avoid-these-patterns-for-fast-unit-tests-220fc225da55 ↩ Effective migration to Kotlin on Android. Aris Papadopoulos. https://medium.com/android-news/effective-migration-to-kotlin-on-android-cfb92bfaa49b ↩ Mocking Kotlin classes with Mockito — the fast way. Brais Gabín Moreira. https://medium.com/21buttons-tech/mocking-kotlin-classes-with-mockito-the-fast-way-631824edd5ba ↩ Did someone try to use Mockk on KMM project. Kotlin Slack. https://slack-chats.kotlinlang.org/t/10131532/did-someone-try-to-use-mockk-on-kmm-project ↩ Mock common tests in kotlin using multiplatform. Stack Overflow. https://stackoverflow.com/questions/65491916/mock-common-tests-in-kotlin-using-multiplatform ↩ Mocking in Kotlin Multiplatform: KSP vs Compiler Plugins. Martin Hristev. https://medium.com/@mhristev/mocking-in-kotlin-multiplatform-ksp-vs-compiler-plugins-4424751b83d7 ↩ Mocks Aren't Stubs. Martin Fowler. https://martinfowler.com/articles/mocksArentStubs.html ↩ Unit Testing — Why must you mock me? Craig Walker. https://medium.com/@walkercp/unit-testing-why-must-you-mock-me-69293508dd13 ↩ Testing on the Toilet: Effective Testing. Google Testing Blog. https://testing.googleblog.com/2014/05/testing-on-toilet-effective-testing.html ↩ Testing strategy and how to test. Now in Android Wiki. https://github.com/android/nowinandroid/wiki/Testing-strategy-and-how-to-test ↩ android/nowinandroid: A fully functional Android app built entirely with Kotlin and Jetpack Compose. GitHub. https://github.com/android/nowinandroid ↩ Flow testing with Turbine. Cash App Code Blog. https://code.cash.app/flow-testing-with-turbine ↩ Templates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse CODE_BLOCK:
@Fake
interface AnalyticsService { fun track(event: String) suspend fun flush(): Result<Unit>
} Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
@Fake
interface AnalyticsService { fun track(event: String) suspend fun flush(): Result<Unit>
} CODE_BLOCK:
@Fake
interface AnalyticsService { fun track(event: String) suspend fun flush(): Result<Unit>
} COMMAND_BLOCK:
val fake = fakeAnalyticsService { track { event -> println("Tracked: $event") } flush { Result.success(Unit) }
} // Use in tests
fake.track("user_signup")
fake.flush() // Verify interactions (thread-safe StateFlow)
assertEquals(1, fake.trackCalls.value.size)
assertEquals(1, fake.flushCalls.value.size) Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK:
val fake = fakeAnalyticsService { track { event -> println("Tracked: $event") } flush { Result.success(Unit) }
} // Use in tests
fake.track("user_signup")
fake.flush() // Verify interactions (thread-safe StateFlow)
assertEquals(1, fake.trackCalls.value.size)
assertEquals(1, fake.flushCalls.value.size) COMMAND_BLOCK:
val fake = fakeAnalyticsService { track { event -> println("Tracked: $event") } flush { Result.success(Unit) }
} // Use in tests
fake.track("user_signup")
fake.flush() // Verify interactions (thread-safe StateFlow)
assertEquals(1, fake.trackCalls.value.size)
assertEquals(1, fake.flushCalls.value.size) CODE_BLOCK:
interface AnalyticsService { fun track(event: String) suspend fun flush(): Result<Unit>
} Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
interface AnalyticsService { fun track(event: String) suspend fun flush(): Result<Unit>
} CODE_BLOCK:
interface AnalyticsService { fun track(event: String) suspend fun flush(): Result<Unit>
} COMMAND_BLOCK:
// Typical handwritten fake — error-prone, tedious
class FakeAnalyticsService( private val trackBehavior: ((String) -> Unit)? = null private val flushBehavior: (suspend () -> Result<Unit>)? = null
) : AnalyticsService { private var _trackCalls = mutableListOf<Unit>() val trackCalls: List<Unit> get() = _trackCalls private var _flushCalls = mutableListOf<Unit>() val flushCalls: List<Unit> get() = _flushCalls // Interface implementation override fun track(event: String) { _trackCalls.add(Unit) trackBehavior?.invoke(event) ?: Unit } override suspend fun flush(): Result<Unit> { _flushCalls.add(Unit) return flushBehavior?.invoke() ?: Result.success(Unit) }
} Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK:
// Typical handwritten fake — error-prone, tedious
class FakeAnalyticsService( private val trackBehavior: ((String) -> Unit)? = null private val flushBehavior: (suspend () -> Result<Unit>)? = null
) : AnalyticsService { private var _trackCalls = mutableListOf<Unit>() val trackCalls: List<Unit> get() = _trackCalls private var _flushCalls = mutableListOf<Unit>() val flushCalls: List<Unit> get() = _flushCalls // Interface implementation override fun track(event: String) { _trackCalls.add(Unit) trackBehavior?.invoke(event) ?: Unit } override suspend fun flush(): Result<Unit> { _flushCalls.add(Unit) return flushBehavior?.invoke() ?: Result.success(Unit) }
} COMMAND_BLOCK:
// Typical handwritten fake — error-prone, tedious
class FakeAnalyticsService( private val trackBehavior: ((String) -> Unit)? = null private val flushBehavior: (suspend () -> Result<Unit>)? = null
) : AnalyticsService { private var _trackCalls = mutableListOf<Unit>() val trackCalls: List<Unit> get() = _trackCalls private var _flushCalls = mutableListOf<Unit>() val flushCalls: List<Unit> get() = _flushCalls // Interface implementation override fun track(event: String) { _trackCalls.add(Unit) trackBehavior?.invoke(event) ?: Unit } override suspend fun flush(): Result<Unit> { _flushCalls.add(Unit) return flushBehavior?.invoke() ?: Result.success(Unit) }
} CODE_BLOCK:
┌──────────────────────────────────────────────────────┐
│ PHASE 1: FIR (Frontend IR) │
│ • Detects @Fake annotations │
│ • Validates interface structure │
│ • Full type system access │
└──────────────────────────────────────────────────────┘ ↓
┌──────────────────────────────────────────────────────┐
│ PHASE 2: IR (Intermediate Representation) │
│ • Analyzes interface methods and properties │
│ • Generates readable .kt source files │
│ • Thread-safe StateFlow call history │
└──────────────────────────────────────────────────────┘ Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
┌──────────────────────────────────────────────────────┐
│ PHASE 1: FIR (Frontend IR) │
│ • Detects @Fake annotations │
│ • Validates interface structure │
│ • Full type system access │
└──────────────────────────────────────────────────────┘ ↓
┌──────────────────────────────────────────────────────┐
│ PHASE 2: IR (Intermediate Representation) │
│ • Analyzes interface methods and properties │
│ • Generates readable .kt source files │
│ • Thread-safe StateFlow call history │
└──────────────────────────────────────────────────────┘ CODE_BLOCK:
┌──────────────────────────────────────────────────────┐
│ PHASE 1: FIR (Frontend IR) │
│ • Detects @Fake annotations │
│ • Validates interface structure │
│ • Full type system access │
└──────────────────────────────────────────────────────┘ ↓
┌──────────────────────────────────────────────────────┐
│ PHASE 2: IR (Intermediate Representation) │
│ • Analyzes interface methods and properties │
│ • Generates readable .kt source files │
│ • Thread-safe StateFlow call history │
└──────────────────────────────────────────────────────┘ - You've already chosen fakes over mocks. If you understand the state-based testing philosophy and prefer testing outcomes over verifying interactions, Fakt automates what you'd otherwise write by hand.
- You only use mocks for convenience. Many developers reach for mocking frameworks not for verify { } features, but simply because writing manual fakes is tedious. Fakt gives you the factory convenience without the mock overhead—generated fakes are plain Kotlin classes.
- You're building for Kotlin Multiplatform. Fakt generates plain Kotlin that compiles on JVM, Native, and WebAssembly—no reflection required. This applies to any source set, not just commonTest.
- You value exercising production code in tests. Fakt-generated fakes are real implementations your tests compile against, catching interface drift at build time rather than runtime.
- Tests run concurrently. Fakt tracks call history with StateFlow, which is thread-safe by design. Manual fakes with var count = 0 break under parallel execution. - You need spy behavior. Partial mocking of real implementations—calling real methods while intercepting others—is something only mocking frameworks can do. Fakt generates new implementations, it doesn't wrap existing ones.
- You're mocking third-party classes without interfaces. If a library exposes final classes with no interface to program against, mocking frameworks can instrument the bytecode. Fakt requires an interface to annotate. - Benchmarking Mockk — Avoid these patterns for fast unit tests. Kevin Block. https://medium.com/@_kevinb/benchmarking-mockk-avoid-these-patterns-for-fast-unit-tests-220fc225da55 ↩
- Effective migration to Kotlin on Android. Aris Papadopoulos. https://medium.com/android-news/effective-migration-to-kotlin-on-android-cfb92bfaa49b ↩
- Mocking Kotlin classes with Mockito — the fast way. Brais Gabín Moreira. https://medium.com/21buttons-tech/mocking-kotlin-classes-with-mockito-the-fast-way-631824edd5ba ↩
- Did someone try to use Mockk on KMM project. Kotlin Slack. https://slack-chats.kotlinlang.org/t/10131532/did-someone-try-to-use-mockk-on-kmm-project ↩
- Mock common tests in kotlin using multiplatform. Stack Overflow. https://stackoverflow.com/questions/65491916/mock-common-tests-in-kotlin-using-multiplatform ↩
- Mocking in Kotlin Multiplatform: KSP vs Compiler Plugins. Martin Hristev. https://medium.com/@mhristev/mocking-in-kotlin-multiplatform-ksp-vs-compiler-plugins-4424751b83d7 ↩
- Mocks Aren't Stubs. Martin Fowler. https://martinfowler.com/articles/mocksArentStubs.html ↩
- Unit Testing — Why must you mock me? Craig Walker. https://medium.com/@walkercp/unit-testing-why-must-you-mock-me-69293508dd13 ↩
- Testing on the Toilet: Effective Testing. Google Testing Blog. https://testing.googleblog.com/2014/05/testing-on-toilet-effective-testing.html ↩
- Testing strategy and how to test. Now in Android Wiki. https://github.com/android/nowinandroid/wiki/Testing-strategy-and-how-to-test ↩
- android/nowinandroid: A fully functional Android app built entirely with Kotlin and Jetpack Compose. GitHub. https://github.com/android/nowinandroid ↩
- Flow testing with Turbine. Cash App Code Blog. https://code.cash.app/flow-testing-with-turbine ↩
how-totutorialguidedev.toaimlgitgithub