Why Fakt?¶
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.
What Fakt Does¶
Fakt reduces fake boilerplate to an annotation:
At compile time, Fakt generates a complete fake implementation. You use it through a type-safe factory:
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.trackCallCount.value)
assertEquals(1, fake.flushCallCount.value)
The Testing Problem¶
Manual Fakes Don’t Scale¶
Consider a simple interface:
A proper, production-quality fake requires ~60-80 lines of boilerplate:
class FakeAnalyticsService : AnalyticsService {
// Behavior configuration
private var trackBehavior: ((String) -> Unit)? = null
private var flushBehavior: (suspend () -> Result<Unit>)? = null
// Call tracking (non-thread-safe!)
private var _trackCallCount = 0
val trackCallCount: Int get() = _trackCallCount
private var _flushCallCount = 0
val flushCallCount: Int get() = _flushCallCount
// Interface implementation
override fun track(event: String) {
_trackCallCount++
trackBehavior?.invoke(event) ?: Unit
}
override suspend fun flush(): Result<Unit> {
_flushCallCount++
return flushBehavior?.invoke() ?: Result.success(Unit)
}
// Configuration methods
fun configureTrack(behavior: (String) -> Unit) {
trackBehavior = behavior
}
fun configureFlush(behavior: suspend () -> Result<Unit>) {
flushBehavior = behavior
}
}
The problems: call tracking uses mutable variables that break under concurrent tests. N methods require ~30N lines. Interface changes don’t break unused fakes—they silently drift. For 50 interfaces, this means thousands of lines of brittle boilerplate.
The Mock Tax¶
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:
| Mocking Pattern | Framework | Comparison | Verified Penalty |
|---|---|---|---|
mockkObject (Singletons) |
MockK | vs. Dependency Injection | 1,391x slower |
mockkStatic (Top-level functions) |
MockK | vs. Interface-based DI | 146x slower |
verify { ... } (Interaction verification) |
MockK | vs. State-based testing | 47x slower |
relaxed mocks (Unstubbed calls) |
MockK | vs. Strict mocks | 3.7x slower |
mock-maker-inline |
Mockito | vs. all-open plugin |
2.7-3x slower122 |
A production test suite with 2,668 tests experienced a 2.7x slowdown (7.3s → 20.0s) when using mock-maker-inline2. For large projects, the mock tax accumulates to 40% slower test suites1.
The KMP Dead End¶
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 Wasm34.
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-project5.
Why Compiler Plugins Work¶
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.
| Aspect | KSP | Compiler Plugin |
|---|---|---|
| Access | After type resolution | During compilation |
| Type System | Read-only symbols | Full manipulation |
| K2 Stability | Broken | Stable |
Fakt uses a two-phase FIR → IR architecture:
┌──────────────────────────────────────────────────────┐
│ 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 tracking │
└──────────────────────────────────────────────────────┘
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.
Why Fakes Over Mocks¶
Beyond performance, fakes represent a different testing philosophy. Martin Fowler’s “Mocks Aren’t Stubs”6 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 details7. 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”8. Mock-based tests often violate this.
Google’s “Now in Android” app makes this explicit9:
“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”10.
Kotlin’s async testing stack—runTest, TestDispatcher, Turbine11—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.
Practical Guidance¶
Fakes vs. Mocks: Quick Comparison¶
| Feature | MockK/Mockito | Fakt |
|---|---|---|
| KMP Support | Limited (JVM only) | Universal (all targets) |
| Compile-time Safety | ❌ | ✅ |
| Runtime Overhead | Heavy (reflection) | Zero |
| Type Safety | Partial (any() matchers) |
Complete |
| Learning Curve | Steep (complex DSL) | Gentle (typed functions) |
| Call Tracking | Manual (verify { }) |
Built-in (StateFlow) |
| Thread Safety | Not guaranteed | StateFlow-based |
| Debuggability | Reflection (opaque) | Generated .kt files |
Choosing the Right Tool¶
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 calls with StateFlow, which is thread-safe by design. Manual fakes with
var count = 0break under parallel execution.
Mocking libraries (Mokkery, MockK) work best when:
-
You need strict call ordering verification. Verifying that
validate()was called beforecharge()beforeship()in exact sequence requiresverify(exhaustiveOrder). Fakt tracks call counts, not call order. -
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.
Get Started¶
- Getting Started - Install Fakt and create your first fake
- Features - Complete feature reference
- Usage Guide - Common patterns and examples
- Migration from Mocks - Moving from MockK/Mockito
Works Cited¶
-
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 ↩↩
-
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 ↩
-
Effective migration to Kotlin on Android. Aris Papadopoulos. https://medium.com/android-news/effective-migration-to-kotlin-on-android-cfb92bfaa49b ↩
-
Reflection | Kotlin Documentation. https://kotlinlang.org/docs/reflection.html ↩
-
Reflection? - Native - Kotlin Discussions. https://discuss.kotlinlang.org/t/reflection/4054 ↩
-
MocKMP: a Mocking processor for Kotlin/Multiplatform. Salomon BRYS. https://medium.com/kodein-koders/mockmp-a-mocking-processor-for-kotlin-multiplatform-51957c484fe5 ↩
-
Trade-offs to consider when choosing to use Mocks vs Fakes. HackMD. https://hackmd.io/@pierodibello/Trade-offs-to-consider-when-choosing-to-use-Mocks-vs-Fakes ↩
-
Testing Kotlin coroutines on Android. Android Developers. https://developer.android.com/kotlin/coroutines/test ↩
-
Why we should use wiremock instead of Mockito. Stack Overflow. https://stackoverflow.com/questions/50726017/why-we-should-use-wiremock-instead-of-mockito ↩
-
Stop Breaking My API: A Practical Guide to Contract Testing with Pact. Medium. https://medium.com/@mohsenny/stop-breaking-my-api-a-practical-guide-to-contract-testing-with-pact-33858d113386 ↩
-
lupuuss/Mokkery: The mocking library for Kotlin Multiplatform. GitHub. https://github.com/lupuuss/Mokkery ↩
-
Kotlin 2.0.0 support · Issue #1 · lupuuss/Mokkery. GitHub. https://github.com/lupuuss/Mokkery/issues/1 ↩
-
Use multiplatform mocking library for tests · Issue #5420 · streetcomplete/StreetComplete. GitHub. https://github.com/streetcomplete/StreetComplete/issues/5420 ↩
-
Kotlin 2.2.0 support · Issue #83 · lupuuss/Mokkery. GitHub. https://github.com/lupuuss/Mokkery/issues/83 ↩
-
Mocking | Mokkery. https://mokkery.dev/docs/Guides/Mocking/ ↩
-
A to Z of Testing in Kotlin Multiplatform. Kinto Technologies. https://blog.kinto-technologies.com/posts/2024-12-24-tests-in-kmp/ ↩
-
Limitations | Mokkery. https://mokkery.dev/docs/Limitations/ ↩