Skip to content
← Todos os posts

Fakt: Automatizando o padrão fake-over-mock

Por 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 mockingFrameworkComparaçãoPenalidade verificada
mockkObject (Singletons)MockKvs. Injeção de Dependência1.391x mais lento
mockkStatic (Funções top-level)MockKvs. DI baseada em interface146x mais lento
verify { ... } (Verificação de interação)MockKvs. Teste baseado em estado47x mais lento
Mocks relaxed (Chamadas sem stub)MockKvs. Mocks estritos3,7x mais lento
mock-maker-inlineMockitovs. plugin all-open2,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.

AspectoKSPCompiler Plugin
AcessoApós a resolução de tiposDurante a compilação
Sistema de tiposSímbolos somente leituraManipulaçã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

RecursoMockK/MockitoFakt
Suporte a KMPLimitado (só JVM)Universal (todos os alvos)
Segurança em compile-time
Overhead em runtimePesado (reflection)Zero
Type safetyParcial (matchers any())Completo
Curva de aprendizadoÍngreme (DSL complexa)Suave (funções tipadas)
Histórico de chamadasManual (verify { })Embutido (StateFlow)
Thread safetyNão garantidaBaseada em StateFlow
Facilidade de debugReflection (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 = 0 quebram 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

  1. 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

  2. Effective migration to Kotlin on Android. Aris Papadopoulos. https://medium.com/android-news/effective-migration-to-kotlin-on-android-cfb92bfaa49b

  3. 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

  4. 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

  5. Mock common tests in kotlin using multiplatform. Stack Overflow. https://stackoverflow.com/questions/65491916/mock-common-tests-in-kotlin-using-multiplatform

  6. Mocking in Kotlin Multiplatform: KSP vs Compiler Plugins. Martin Hristev. https://medium.com/@mhristev/mocking-in-kotlin-multiplatform-ksp-vs-compiler-plugins-4424751b83d7

  7. Mocks Aren’t Stubs. Martin Fowler. https://martinfowler.com/articles/mocksArentStubs.html

  8. Unit Testing — Why must you mock me? Craig Walker. https://medium.com/@walkercp/unit-testing-why-must-you-mock-me-69293508dd13

  9. Testing on the Toilet: Effective Testing. Google Testing Blog. https://testing.googleblog.com/2014/05/testing-on-toilet-effective-testing.html

  10. Testing strategy and how to test. Now in Android Wiki. https://github.com/android/nowinandroid/wiki/Testing-strategy-and-how-to-test

  11. android/nowinandroid: A fully functional Android app built entirely with Kotlin and Jetpack Compose. GitHub. https://github.com/android/nowinandroid

  12. Flow testing with Turbine. Cash App Code Blog. https://code.cash.app/flow-testing-with-turbine