Voltar
17.05.2026

Testes em Flutter: anotações e práticas que uso

Um conjunto de práticas, aprendizados e anotações que venho acumulando escrevendo testes em Flutter.

  • testes
  • Flutter

Sumário

Introdução

Esse texto reúne anotações e práticas que venho acumulando ao escrever testes em Flutter.

Não é um guia definitivo nem uma regra sobre como testes devem ser feitos. É só um recorte do que tem funcionado para mim e do que venho refinando nos projetos em que trabalhei.

Os exemplos usados aqui foram adaptados principalmente do projeto dhianapereira/rick-and-morty. Em alguns pontos simplifiquei os trechos, mas preservei a estrutura, as ferramentas e o estilo dos testes que realmente uso.

Organização e convenções

Com o tempo fui percebendo que a parte mais difícil dos testes raramente é escrever o primeiro. O mais trabalhoso costuma ser manter a suíte saudável quando o projeto cresce.

Por isso, hoje tento priorizar previsibilidade, isolamento e legibilidade.

Testes precisam ser simples de ler

Legibilidade pesa bastante para mim.

Na maior parte do tempo alguém vai abrir aquele teste tentando responder perguntas como:

  • o que esse código deveria fazer?
  • qual comportamento está sendo validado?
  • o que quebrou?

Por isso, prefiro testes mais explícitos do que soluções muito abstratas ou cheias de camadas.

Nomenclatura

Costumo tratar nome de teste quase como documentação.

No rick-and-morty, o padrão mais recorrente ficou próximo de:

Should expose loaded episodes when initial page load succeeds

ou:

Should retry loading when try again button is tapped

Não existe um formato perfeito, mas gosto quando o nome deixa claro:

  • comportamento
  • contexto
  • resultado esperado

Cada teste deve validar uma coisa principal

Quando um teste tenta validar muitas coisas ao mesmo tempo, ele costuma ficar:

  • difícil de manter
  • difícil de entender
  • difícil de depurar

Então prefiro separar cenários em testes menores, mesmo quando eles compartilham boa parte da preparação.

AAA e Given / When / Then

Quando o teste cresce, gosto de separar a leitura usando AAA:

// Arrange
when(
  () => repository.fetchPage(1),
).thenAnswer((_) async => _buildEpisodePage(page: 1));

// Act
await sut.loadInitialPage();

// Assert
expect(sut.value.currentPage, 1);
expect(sut.value.episodes.length, 10);

Em cenários maiores, principalmente em widget e integração, também gosto de pensar em Given / When / Then, mesmo que eu não deixe isso comentado no arquivo.

Se você quiser se aprofundar especificamente nesses dois formatos, deixei algumas referências sobre AAA e Given/When/Then no fim do texto.

Estrutura das pastas

Na pasta test/, costumo espelhar a estrutura da lib/.

lib/
└── features/
    └── episodes/
        └── presentation/
            └── pages/
                └── episode_list_page.dart

test/
└── features/
    └── episodes/
        └── presentation/
            └── pages/
                └── episode_list_page_test.dart

Isso facilita bastante quando o projeto cresce, porque fica simples localizar o teste relacionado a uma feature.

Dummies, fakes e mocks

Durante muito tempo eu usava “mock” para tudo, mas com o tempo fui separando melhor as responsabilidades.

Dummy

Objeto usado só para preencher um parâmetro quando o valor em si não importa.

const episode = Episode(
  id: 1,
  name: 'Pilot',
  code: 'S01E01',
  airDate: 'December 2, 2013',
);

Fake

Implementação simplificada de algo real.

class FakeEpisodeRepository implements EpisodeRepository {
  @override
  Future<EpisodePage> fetchPage(int page) async {
    return const EpisodePage(
      currentPage: 1,
      totalPages: 1,
      totalEpisodes: 1,
      episodes: <Episode>[
        Episode(
          id: 1,
          name: 'Pilot',
          code: 'S01E01',
          airDate: 'December 2, 2013',
        ),
      ],
    );
  }
}

Mock

Objeto usado para controlar respostas e validar interações.

class _MockEpisodeRepository extends Mock implements EpisodeRepository {}

Na maioria dos projetos uso bastante a lib mocktail.

Mocks compartilhados

Tenho evitado arquivos globais enormes de mocks compartilhados.

Na maioria das vezes prefiro criar o mock no próprio arquivo de teste:

class _MockEpisodeListController extends Mock
    implements EpisodeListController {}

Isso deixa o teste mais independente e reduz atrito com importações desnecessárias.

Dependências por construtor

Nos testes unitários, prefiro receber dependências por construtor:

sut = EpisodeListController(
  repository: repository,
  searchDebounceDuration: const Duration(milliseconds: 1),
);

Aqui também vale uma observação sobre nome. sut é uma sigla para system under test, ou seja, o objeto principal que está sendo testado naquele cenário.

Eu não uso esse nome em absolutamente todo projeto, mas ele aparece com frequência em exemplos de teste e também no rick-and-morty, então achei melhor manter esse padrão nos trechos do texto.

Esse tipo de composição deixa muito mais simples montar cenários isolados sem depender de estado global.

Encapsular libs externas

Também gosto de evitar dependências externas espalhadas pela regra de negócio.

Em vez de deixar uma feature acoplada diretamente a uma biblioteca de HTTP, prefiro concentrar isso em uma camada própria. Assim, a regra da aplicação conversa com interfaces mais previsíveis, e os testes ficam mais baratos de escrever e manter.

setUp, tearDown e isolamento

Quase todos os testes que escrevo usam setUp.

Nos testes unitários do rick-and-morty, por exemplo, a base costuma ser algo assim:

late EpisodeRepository repository;
late EpisodeListController sut;

setUp(() {
  repository = _MockEpisodeRepository();
  sut = EpisodeListController(
    repository: repository,
    searchDebounceDuration: const Duration(milliseconds: 1),
  );
});

Em testes de widget e golden, também uso tearDown com frequência para limpar estado global, principalmente quando registro dependências no GetIt.

tearDown(() async {
  stateNotifier.dispose();
  await GetIt.I.reset();
});

Quase nunca uso setUpAll.

Ele pode ajudar em alguns cenários, mas prefiro manter cada teste totalmente independente. Isso reduz o risco de vazamento de estado e deixa as falhas mais fáceis de interpretar.

Rodar testes em ordem aleatória

Sempre que dá, gosto de rodar a suíte em ordem aleatória.

Quando um teste só passa porque outro rodou antes, quase sempre existe algum estado vazando. Esse tipo de problema aparece cedo quando a ordem de execução deixa de ser previsível.

Testes unitários

Testes unitários continuam sendo a base da maior parte do que escrevo.

São rápidos, têm custo baixo de manutenção e costumam cobrir boa parte das regras que mais quebram com a evolução do código.

O que costumo priorizar

Geralmente foco em:

  • regras de negócio
  • controllers e use cases
  • repositories
  • transformações de dados
  • tratamento de erro
  • estados
  • fluxos críticos

O que geralmente não me traz tanto retorno

Existem alguns cenários em que costumo ver menos valor:

  • classes muito simples
  • widgets muito pequenos e sem comportamento
  • integrações acopladas diretamente a SDKs, sem uma interface intermediária

Quando não existe uma abstração clara, às vezes prefiro validar o comportamento em outro nível de teste.

Exemplo

Esse é um recorte bem próximo do que está no projeto:

test(
  'Should expose error message when repository throws exception',
  () async {
    when(
      () => repository.fetchPage(1),
    ).thenThrow(const EpisodeException('Unable to fetch episodes.'));

    await sut.loadInitialPage();

    expect(sut.value.hasError, isTrue);
    expect(sut.value.errorMessage, 'Unable to fetch episodes.');
    expect(sut.value.hasContent, isFalse);
  },
);

Uma coisa que tento evitar é colocar lógica demais dentro do próprio teste. Quando ele começa a parecer uma implementação paralela da feature, isso costuma indicar que o cenário ficou complexo demais.

Testes de widget

Testes de widget são, de longe, um dos níveis que mais gosto de usar.

Eles conseguem validar comportamento, renderização e interação sem o custo de um teste de integração completo.

O que costumo validar

Geralmente foco em:

  • renderização básica
  • interações
  • navegação
  • loading
  • mensagens de erro
  • atualização de estado

Na maior parte do tempo me preocupo mais com comportamento do que com detalhes internos da implementação.

GetIt nesse nível

Em testes unitários, prefiro depender de construtor. Já em testes de widget, se o widget real usa GetIt, não me incomodo em usar o mesmo caminho no teste.

No rick-and-morty, esse padrão aparece bastante:

setUp(() {
  controller = _MockEpisodeListController();
  GetIt.I.registerSingleton<EpisodeListController>(controller);
});

Isso deixa o teste mais próximo da forma como a tela realmente funciona na aplicação.

Widgets com animação

Animações costumam ser uma parte mais chata de testar.

Quando preciso lidar com isso, tento manter o teste previsível: controlo bem o uso de pump, evito depender cegamente de pumpAndSettle e, se fizer sentido, reduzo ou neutralizo a animação no ambiente de teste.

Exemplo

Esse é um dos cenários do episode_list_page_test.dart:

testWidgets('Should retry loading when try again button is tapped', (
  WidgetTester tester,
) async {
  stateNotifier.value = const EpisodeListState(
    errorMessage: 'Temporary failure.',
  );

  when(() => controller.retry()).thenAnswer((_) async {
    stateNotifier.value = _buildLoadedState(page: 1);
  });

  await tester.pumpWidget(_buildTestApp());
  await tester.pump();

  expect(find.text('Unable to load episodes'), findsOneWidget);

  await tester.tap(find.widgetWithText(FilledButton, 'Try again'));
  await tester.pump();

  expect(find.text('Episode 1'), findsOneWidget);
  verify(() => controller.retry()).called(1);
});

Esse tipo de teste me agrada porque valida o que a pessoa usuária enxerga e faz, sem depender demais de detalhes internos.

Golden tests com Alchemist

Golden tests passaram a fazer mais sentido para mim quando comecei a usá-los para validar regressões visuais maiores, e não como substituto de todo teste de widget.

Atualmente, a biblioteca que uso para isso é o alchemist.

Por que tenho preferido alchemist

No fluxo que venho usando, o alchemist deixou os cenários mais claros e a configuração mais previsível, especialmente por causa da forma como lida com cenários, temas e execução em CI.

No rick-and-morty, a configuração global fica em test/flutter_test_config.dart:

return AlchemistConfig.runWithConfig(
  config: AlchemistConfig(
    platformGoldensConfig: PlatformGoldensConfig(
      platforms: _platforms,
      diffThreshold: 0.003,
    ),
    ciGoldensConfig: CiGoldensConfig(diffThreshold: 0.001),
  ),
  run: () async {
    await testMain();
  },
);

O que costumo cobrir

No projeto, tenho usado golden principalmente para:

  • telas completas
  • estados importantes, como carregado e erro
  • diferenças de tema, como light e dark

Tenho evitado criar golden para widgets muito pequenos, porque o custo de manutenção sobe rápido e o retorno nem sempre compensa.

Tamanho de tela

No estado atual do rick-and-morty, os goldens estão padronizados em um cenário mobile fixo por meio de um helper:

GoldenTestGroup buildMobileGoldenScenario({
  required Widget child,
  String name = 'mobile',
  int columns = 1,
}) {
  return GoldenTestGroup(
    columns: columns,
    children: <Widget>[
      GoldenTestScenario(
        name: name,
        child: SizedBox(width: 390, height: 844, child: child),
      ),
    ],
  );
}

Ou seja: neste projeto eu não estou validando vários breakpoints nos goldens. Se eu precisar cobrir outros tamanhos, prefiro adicionar isso de forma explícita, em vez de transformar cada snapshot em uma matriz enorme.

Exemplo

Esse é um exemplo fiel ao estilo usado no repositório:

goldenTest(
  'Should render loaded episode list page when light theme is active',
  fileName: 'episode_list_page_loaded_light',
  builder: () {
    stateNotifier.value = _buildLoadedState();

    return buildMobileGoldenScenario(
      child: const Scaffold(
        body: EpisodeListPage(),
      ).toTestApp(theme: AppTheme.light),
    );
  },
);

Além desse cenário, o projeto também cobre o estado de erro e o tema escuro.

Sobre manutenção

Golden é ótimo para pegar regressão visual, mas também pode ficar frágil com facilidade.

Por isso, tento evitar:

  • excesso de snapshots
  • cenários pequenos demais
  • validação de detalhes irrelevantes
  • atualização de imagem sem revisar a mudança visual

Testes de integração

Também uso testes de integração principalmente para validar fluxos críticos.

Não tento cobrir tudo nesse nível, porque esse tipo de teste costuma ser mais lento, mais caro de manter e mais sensível a pequenas mudanças.

O que costumo validar

Geralmente foco em:

  • caminho feliz
  • navegação entre telas
  • integrações que só fazem sentido ponta a ponta

Exemplo do fluxo principal

No rick-and-morty, o teste de integração principal valida algo próximo disto:

abrir app
→ avançar para a próxima página da lista de episódios
→ buscar por "Pilot"
→ abrir os detalhes do episódio
→ tocar em um personagem
→ validar a tela de detalhes do personagem

O nome do teste no projeto deixa isso bem claro:

Should navigate from episode list to character details When user completes the main flow

Para mim, esse tipo de teste funciona melhor quando cobre o fluxo central da aplicação sem tentar resolver todas as variações possíveis de uma vez.

Referências

Esses materiais me ajudam bastante quando estudo ou revisito testes em Flutter:

Existem muitas formas válidas de organizar testes.

Essas são só algumas práticas e anotações que vêm fazendo sentido para mim até aqui. :)