Por que removi mockito do codebase Flutter e troquei por Valenty (e o que ganhei)
Mockito exige build_runner e gera centenas de linhas de boilerplate. Troquei por fakes manuais + Valenty e o tempo de teste local caiu de 45s pra 8s.
Resposta direta: tirei o mockito de uma base Flutter porque o gargalo nunca foi escrever o mock — era o build_runner. O mockito gera os mocks por code generation: você anota com @GenerateMocks, roda dart run build_runner build, e ele cospe um arquivo .mocks.dart por suite. A própria documentação oficial do Flutter é explícita: "mockito 5.0.0 suporta null safety graças a code generation. Pra rodar a geração de código necessária, adicione a dependência build_runner" (Flutter docs, 2026). Em projeto pequeno isso passa batido. Numa base de 40 mil linhas que eu herdei, o build_runner virou um pedágio de 12 a 25 segundos toda vez que uma interface mudava. Troquei tudo por fakes manuais mais o Valenty (um framework de teste que publiquei no pub.dev). O tempo de teste local na minha máquina caiu de 45s pra 8s — porque o code generation simplesmente sumiu do loop.
Eu não cheguei nessa decisão lendo thread no Reddit. Cheguei depois de apagar um arquivo de mocks de 800 linhas que existia só pra cobrir 5 dependências. Esse post é o porquê, com números, e onde eu ainda manteria o mockito.
O problema do mockito não é o mock — é o codegen
Deixa eu separar duas coisas que vivem confundidas. O mock em si — um dublê que registra chamadas e devolve valores stubados — é uma ideia ótima. O mecanismo que o mockito usa pra criar esse dublê em Dart, desde o null safety, é code generation. E é aí que mora o custo.
O build_runner é um imposto que cresce com o projeto
O build_runner analisa os arquivos Dart do projeto pra decidir o que regenerar. Quanto mais o projeto cresce, mais arquivos ele varre, e mais lento fica — esse é um problema documentado à exaustão pela comunidade. O Code With Andrea tem um guia inteiro só sobre acelerar code generation, e a recomendação número um é literalmente "use a opção generate_for pra dizer exatamente quais arquivos o builder deve processar" (Code With Andrea, 2026). Pensa no que isso significa: a ferramenta é tão lenta por padrão que a melhor prática vira configurar manualmente o que ela NÃO deve olhar.
Tem relato público de um app Flutter com ~40 devs e 3.000 testes onde a geração levava 8 minutos num Mac M1, 16 minutos numa box Linux de CI, e simplesmente falhava (exit code -9, sem memória) numa box de 4GB. O time só conseguiu derrubar isso de 12 pra ~5 minutos renomeando os fontes com sufixo .buildable e configurando generate_for (MobileNativeFoundation, discussão #200, 2026). Cinco minutos é o estado "otimizado". Pra regenerar mock.
No app de 40k linhas, o número não era de minutos, era de 12 a 25 segundos por rodada de build_runner — toda vez que eu mexia numa assinatura de método que tinha mock. Multiplica isso pelas dezenas de vezes que você muda uma interface num dia de refactor. O custo não é o segundo isolado. É o microcorte de fluxo, repetido, que faz você parar de rodar teste antes de commitar. Esse é o jeito errado de gastar a atenção de um dev sênior.
Erro opaco quando o stub não casa
O segundo problema é mais traiçoeiro. Quando você esquece de stubar um método num mock gerado por @GenerateMocks, o comportamento padrão é estourar exceção — e a mensagem fala da implementação do mock, não do seu cenário. A doc do próprio mockito diz que o @GenerateNiceMocks (o recomendado hoje) devolve "um valor legal simples" pra chamadas não stubadas (Flutter docs, 2026). Ótimo pra não quebrar — péssimo pra debugar. Seu teste passa devolvendo 0, '' ou null num lugar que você nem sabia que tava sendo chamado, e o bug vaza pra produção mascarado de teste verde.
Eu já perdi uma tarde inteira por causa disso. O mock devolvia o valor "legal" default, o teste ficava verde, e a regra real tava errada. Que é exatamente o cenário que a Valentina Jemuovic descreve quando explica por que a velha pirâmide de teste falha:
"Seus testes de unidade passam. Seus testes E2E passam. E mesmo assim o cálculo de imposto tava errado. [...] Tem um abismo enorme entre 'todos os meus testes unitários passam' e 'essa feature funciona como o cliente espera'. Esse abismo é onde seus bugs de produção vivem." — Valentina Jemuovic, Optivem Journal
Manual fakes: mesma cobertura, zero codegen
A alternativa que ninguém quer ouvir porque soa como trabalho braçal: fake manual. Uma classe por dependência, implementando só os métodos que aquele teste usa. Sem anotação, sem .mocks.dart, sem rodar nada.
A objeção imediata é "mas é mais linha de código". Na prática, não é. O arquivo de mock gerado tinha 800 linhas pra cobrir 5 dependências — código que eu não escrevi nem li, mas que entrava no diff e no tempo de build. O fake manual equivalente são umas 15-30 linhas por dependência, escritas por mim, legíveis, e que param de existir como artefato gerado. Linha de código que você controla é diferente de linha de código que uma ferramenta vomita.
E o erro fica legível. Se o fake não implementa um método, o compilador Dart te avisa na hora, com o nome da classe e do método — não em runtime, não com valor "legal" silencioso. Compile error é o melhor tipo de bug: ele acontece antes de você rodar qualquer coisa.
// Fake manual: 0 codegen, erro de compilação se a interface mudar
class FakeOrderApi implements OrderApi {
final List<Order> _orders;
FakeOrderApi(this._orders);
@override
Future<List<Order>> fetchOrders() async => _orders;
// Se OrderApi ganhar um método novo, isto aqui PARA DE COMPILAR.
// O mockito gerado teria devolvido um valor "legal" e mascarado.
}
valentyTest: o pulo do gato pra teste de componente
Fake manual resolve o "como eu paro o code generation". O Valenty resolve o "como eu paro de testar implementação no lugar de comportamento". O Valenty é um pacote open-source que publiquei no pub.dev (valenty_test + valenty_cli) depois de me cansar do boilerplate. Ele é construído em cima da Modern Test Pyramid da Valentina Jemuovic, e cobre o nível de component test: roda o app Flutter inteiro, com a lógica de negócio de verdade, mas com as dependências externas (Firebase, Dio, banco) trocadas por fakes.
O padrão valentyTest separa três coisas que num teste de widget normal viram uma sopa só:
- BackendStubDsl — o
setup, onde você configura o que os sistemas externos devolvem. - SystemDsl — o
body, onde você descreve as ações do usuário em linguagem de domínio. - UiDriver — a camada chata, onde mora todo
find.byKey,tester.tapepumpAndSettle.
O teste em si fica assim — repara que não tem um único find.byKey no corpo:
valentyTest(
'deve mostrar o total do pedido após finalizar',
setup: (backend) {
backend.stubProduct(sku: 'APPLE1001', price: 2.50);
backend.stubOrderCreation(totalPrice: 12.50);
},
body: (system, backend) async {
await system.openApp();
await system.selectProduct('APPLE1001');
await system.setQuantity(5);
await system.placeOrder();
await system.verifyConfirmation('Total: R\$ 12,50');
},
);
Isso lê como uma história de usuário. Daqui a seis meses, quando você voltar nesse teste, vai entender o cenário sem decodificar árvore de widget. E quando o botão "finalizar" mudar de key, você conserta num lugar só — o UiDriver — e todos os cenários continuam passando. O mock gerado não te dá essa separação: ele te empurra pra verificar "o método X foi chamado com Y", que é testar implementação, não comportamento.
Por que não é só "fakes com nome bonito"
A diferença de fundo é o que o teste afirma. Com mockito é fácil cair em verify(mock.placeOrder(any)).called(1) — você tá testando que uma chamada aconteceu. Com o valentyTest você afirma verifyConfirmation('Total: R$ 12,50') — que é o que o usuário vê. Quando o refactor troca a sequência interna de chamadas mas o resultado pro usuário continua certo, o teste mockito quebra (falso negativo) e o teste Valenty continua verde. Esse é o ponto inteiro da Modern Test Pyramid: testar a borda do comportamento, não o miolo da implementação.
A migração real: 40k linhas, 3 sprints, 45s → 8s
Não foi big bang. Num app Flutter de 40 mil linhas que eu mantenho, a migração rodou em 3 sprints, feature por feature:
| Sprint | O que migrou | Resultado |
|---|---|---|
| 1 | Features novas já nasceram com valentyTest; mockito congelado |
Time parou de gerar mock novo |
| 2 | Hot paths (checkout, auth) — os testes que rodavam mais | build_runner saiu do loop desses módulos |
| 3 | Resto da base + remoção do build_runner das deps de teste |
Tempo de teste local: 45s → 8s |
Os 8 segundos não são mágica de performance do Valenty — são a ausência do code generation. Tirar o build_runner do caminho é o que devolve o tempo. O Valenty é o que faz os testes valerem a pena de manter depois.
Custo honesto da migração: cada feature levou de meio a um dia pra reescrever os testes no formato valentyTest, e teve atrito no começo porque o time tava acostumado com when(...).thenReturn(...). Não vendo isso como grátis. Vendo como pago uma vez e cobrado nunca mais — diferente do build_runner, que cobra um pedágio toda rodada, pra sempre.
Quando eu ainda usaria mockito (ou mocktail)
Não vou fingir que mock gerado é sempre errado. Se eu precisasse verificar interação fina — tipo "esse método de billing foi chamado exatamente uma vez, com esse argumento, nessa ordem" — o verify() do mockito é direto ao ponto e eu não reinventaria isso à mão. Pra esse caso, aliás, eu preferiria mocktail, que faz a mesma coisa sem code generation (mocktail, pub.dev, 2026) — o que mata metade do meu problema de cara. Se a sua dor é só o build_runner e você ama a API de mock, troca mockito por mocktail e siga a vida.
Mas pra teste de componente — rodar a feature de ponta a ponta com fakes — eu não volto pro mock gerado. Verificar comportamento de usuário com verify(mock).called(1) é a ferramenta errada pro trabalho. Aí o valentyTest ganha.
Uma ressalva de transparência: o valenty_test tá em pré-release (v0.2.3) e tem 160 de 160 pub points no pub.dev — formato, doc e exemplos no lugar, o máximo da régua de qualidade (valenty_test, pub.dev, 2026). Mas é novo, com pouca adoção de comunidade ainda. Eu uso em produção porque eu escrevi e confio, mas não vou te vender base instalada que ainda não existe.
Como começar em 30 segundos
O CLI é só instalador — quem faz o trabalho pesado é a IA do seu editor, que lê os skill files gerados e aprende a arquitetura valentyTest:
dart pub global activate valenty_cli && cd seu_app_flutter && valenty init
O valenty init detecta o projeto Flutter, adiciona valenty_test como dev dependency, cria o .valenty.yaml e gera os arquivos de skill pro Claude Code, Cursor ou Codex. Depois você pede pro seu agente: "scaffold a feature Pedido pro valentyTest" e "escreve o teste: usuário adiciona item e vê o total". Roda flutter test. Sem build_runner no meio.
O resumo cabe numa frase: o mock não era o problema, o code generation era. Tira o build_runner do loop e você ganha de volta o tempo que nem sabia que tava perdendo.
Se você tem um app Flutter com suite de teste lenta por causa de codegen — ou nenhuma suite porque "dá trabalho demais" — é exatamente o tipo de coisa que a Hens resolve. Manda mensagem.
Fontes
- Mock dependencies using Mockito — Flutter docs (2026)
- mockito — Dart package (pub.dev, 2026)
- mocktail — Dart package (pub.dev, 2026)
- valenty_test — Dart package (pub.dev, 2026)
- Null safety with Mockito — dart-lang/mockito (2026)
- Speed up code generation with build_runner — Code With Andrea (2026)
- Reducing build_runner generation time in a large Flutter app — MobileNativeFoundation Discussion #200 (2026)
- Modern Test Pyramid — Valentina Jemuovic, Optivem Journal
Quer um app assim pro seu negócio?
A Hens constrói apps Flutter, bots WhatsApp com IA e backoffices sob medida — com o mesmo rigor dos nossos produtos próprios. Do MVP ao app em produção. Primeira conversa é direta, sem formulário.
Falar no WhatsApp →