Fakt: Automatizando o padrão fake-over-mock
Por Rodrigo Sicarelli 8 min de leitura
Os testes em Kotlin têm um problema que piora quanto mais bem-sucedido seu projeto se torna.
Fakes de teste escritos à mão não escalam — cada interface exige de 60 a 80 linhas de boilerplate que silenciosamente se distancia da realidade durante refatorações. Frameworks de mocking em runtime (MockK, Mockito) resolvem o boilerplate, mas introduzem penalidades severas de performance e não funcionam em Kotlin/Native ou WebAssembly. Ferramentas baseadas em KSP prometiam geração em tempo de compilação, mas o Kotlin 2.0 quebrou todas elas.
Fakt é um compiler plugin que gera fakes com qualidade de produção por meio de uma integração profunda com as fases de compilação FIR e IR do Kotlin — os mesmos pontos de extensão usados pelo Metro, um framework de DI de produção criado por Zac Sweers.
O que o Fakt faz
https://github.com/rsicarelli/fakt
O Fakt reduz o boilerplate de um fake a uma annotation:
@Fake
interface AnalyticsService {
fun track(event: String)
suspend fun flush(): Result<Unit>
}
Em tempo de compilação, o Fakt gera uma implementação fake completa. Você a utiliza por meio de uma factory type-safe:
val fake = fakeAnalyticsService {
track { event -> println("Tracked: $event") }
flush { Result.success(Unit) }
}
// Use nos testes
fake.track("user_signup")
fake.flush()
// Verifique as interações (StateFlow thread-safe)
assertEquals(1, fake.trackCalls.value.size)
assertEquals(1, fake.flushCalls.value.size)
É só isso ✨
O problema do teste
Considere uma interface simples:
interface AnalyticsService {
fun track(event: String)
suspend fun flush(): Result<Unit>
}
Um fake completo, com qualidade de produção, exige de 40 a 60 linhas de boilerplate:
// Fake típico escrito à mão — propenso a erros, tedioso
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
// Implementação da interface
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)
}
}
Os problemas: N métodos exigem cerca de 10N linhas. Mudanças na interface não quebram fakes não utilizados — eles silenciosamente se distanciam da realidade. Para 50 interfaces, isso significa milhares de linhas de boilerplate frágil.
O imposto do mock
Frameworks de mocking em runtime resolvem o boilerplate, mas pagam um preço diferente. Classes em Kotlin são final por padrão, então MockK e Mockito recorrem à instrumentação de bytecode. Benchmarks independentes1 quantificam a penalidade:
| Padrão de mocking | Framework | Comparação | Penalidade verificada |
|---|---|---|---|
mockkObject (Singletons) | MockK | vs. Injeção de Dependência | 1.391x mais lento |
mockkStatic (Funções top-level) | MockK | vs. DI baseada em interface | 146x mais lento |
verify { ... } (Verificação de interação) | MockK | vs. Teste baseado em estado | 47x mais lento |
Mocks relaxed (Chamadas sem stub) | MockK | vs. Mocks estritos | 3,7x mais lento |
mock-maker-inline | Mockito | vs. plugin all-open | 2,7-3x mais lento23 |
Uma suíte de testes de produção com 2.668 testes sofreu uma desaceleração de 2,7x (7,3s → 20,0s) ao usar mock-maker-inline3. Em projetos grandes, o imposto do mock se acumula em suítes de teste 40% mais lentas1.
O beco sem saída do KMP
O mocking em runtime depende de recursos específicos da JVM: reflection, instrumentação de bytecode, dynamic proxies. Kotlin/Native e Kotlin/Wasm compilam para código de máquina. Não existe JVM. MockK e Mockito não conseguem rodar em source sets commonTest que tenham como alvo Native ou Wasm45.
A comunidade tentou soluções baseadas em KSP, mas o compilador K2 do Kotlin 2.0 as quebrou. O app StreetComplete (mais de 10.000 testes) foi forçado a migrar no meio do projeto6.
Por que compiler plugins funcionam
Ferramentas baseadas em KSP (Mockative, MocKMP) operavam no nível de símbolos — depois da resolução de tipos, com acesso limitado ao sistema de tipos. Quando o K2 chegou, elas quebraram. Compiler plugins operam durante a compilação, com acesso completo a FIR e IR. Eles sobrevivem às atualizações de versão do Kotlin.
| Aspecto | KSP | Compiler Plugin |
|---|---|---|
| Acesso | Após a resolução de tipos | Durante a compilação |
| Sistema de tipos | Símbolos somente leitura | Manipulação completa |
O Fakt usa uma arquitetura de duas fases, FIR → IR:
┌──────────────────────────────────────────────────────┐
│ FASE 1: FIR (Frontend IR) │
│ • Detecta annotations @Fake │
│ • Valida a estrutura da interface │
│ • Acesso completo ao sistema de tipos │
└──────────────────────────────────────────────────────┘
↓
┌──────────────────────────────────────────────────────┐
│ FASE 2: IR (Intermediate Representation) │
│ • Analisa métodos e propriedades da interface │
│ • Gera arquivos-fonte .kt legíveis │
│ • Histórico de chamadas com StateFlow thread-safe │
└──────────────────────────────────────────────────────┘
Esse é o mesmo padrão usado pelo Metro, o compiler plugin de DI de Zac Sweers. A arquitetura do Metro se mostrou estável ao longo do Kotlin 1.9, 2.0 e 2.1.
Por que fakes em vez de mocks
Além da performance, fakes representam uma filosofia de teste diferente. O artigo “Mocks Aren’t Stubs”, de Martin Fowler7, descreve duas escolas: teste baseado em estado (verificar resultados) e teste baseado em interação (verificar chamadas de método).
O problema dos testes baseados em interação: eles se acoplam a detalhes de implementação8. Refatore a assinatura de um método sem mudar o comportamento e os testes baseados em mock quebram. O Testing Blog do Google define resiliência como uma qualidade crítica de um teste — “um teste não deveria falhar se o código sob teste não está com defeito”9. Testes baseados em mock frequentemente violam isso.
O app “Now in Android” do Google deixa isso explícito10:
“Não use frameworks de mocking. Em vez disso, use fakes.”
O objetivo: “testes menos frágeis que podem exercitar mais código de produção, em vez de apenas verificar chamadas específicas contra mocks”11.
A stack de teste assíncrono do Kotlin — runTest, TestDispatcher, Turbine12 — é inerentemente baseada em estado. O awaitItem() do Turbine verifica valores emitidos, não chamadas de método. A fonte de dados natural para essa stack é um fake apoiado em MutableStateFlow. O Fakt automatiza esse padrão.
Orientações práticas
Fakes vs. Mocks: comparação rápida
| Recurso | MockK/Mockito | Fakt |
|---|---|---|
| Suporte a KMP | Limitado (só JVM) | Universal (todos os alvos) |
| Segurança em compile-time | ❌ | ✅ |
| Overhead em runtime | Pesado (reflection) | Zero |
| Type safety | Parcial (matchers any()) | Completo |
| Curva de aprendizado | Íngreme (DSL complexa) | Suave (funções tipadas) |
| Histórico de chamadas | Manual (verify { }) | Embutido (StateFlow) |
| Thread safety | Não garantida | Baseada em StateFlow |
| Facilidade de debug | Reflection (opaco) | Arquivos .kt gerados |
Escolhendo a ferramenta certa
O Fakt e as bibliotecas de mocking resolvem problemas sobrepostos, mas distintos. A escolha entre eles depende das suas restrições e necessidades de teste.
O Fakt funciona melhor quando:
-
Você já escolheu fakes em vez de mocks. Se você entende a filosofia do teste baseado em estado e prefere testar resultados em vez de verificar interações, o Fakt automatiza o que você escreveria à mão.
-
Você usa mocks apenas por conveniência. Muitos desenvolvedores recorrem a frameworks de mocking não pelos recursos de
verify { }, mas simplesmente porque escrever fakes à mão é tedioso. O Fakt te dá a conveniência da factory sem o overhead do mock — os fakes gerados são classes Kotlin comuns. -
Você está construindo para Kotlin Multiplatform. O Fakt gera Kotlin puro que compila em JVM, Native e WebAssembly — sem reflection. Isso vale para qualquer source set, não só o
commonTest. -
Você valoriza exercitar código de produção nos testes. Os fakes gerados pelo Fakt são implementações reais contra as quais seus testes compilam, capturando o desvio da interface em tempo de build, e não em runtime.
-
Os testes rodam concorrentemente. O Fakt rastreia o histórico de chamadas com StateFlow, que é thread-safe por design. Fakes à mão com
var count = 0quebram sob execução paralela.
Bibliotecas de mocking (Mokkery, MockK) funcionam melhor quando:
-
Você precisa de comportamento de spy. O mocking parcial de implementações reais — chamar métodos reais enquanto intercepta outros — é algo que só frameworks de mocking conseguem fazer. O Fakt gera novas implementações; ele não envolve as existentes.
-
Você está mockando classes de terceiros sem interfaces. Se uma biblioteca expõe classes final sem nenhuma interface contra a qual programar, frameworks de mocking podem instrumentar o bytecode. O Fakt exige uma interface para anotar.
Nenhuma das ferramentas substitui o contract testing. Para APIs HTTP de terceiros, use WireMock ou Pact. Fakes escritos à mão para serviços externos se distanciam da realidade sem validação de contrato — eles criam ilusões perigosas de fidelidade que quebram em produção.
Referências
Footnotes
-
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 ↩ ↩2
-
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 ↩ ↩2
-
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 ↩