Qualidade é feedback em camadas
Testes não provam ausência de defeitos; eles reduzem incerteza sobre comportamentos importantes. Uma estratégia equilibrada usa muitos testes rápidos de domínio, testes de widget para comportamento visual e poucos fluxos integrados de alto valor.
Testes unitários
test('aplica desconto de dez por cento', () {
const calculator = DiscountCalculator();
final result = calculator.apply(total: 100, percentage: 10);
expect(result, 90);
});
Estruture em Arrange, Act e Assert e nomeie pelo comportamento. Um teste deve falhar por uma razão clara.
Fakes costumam ser melhores para repositórios simples porque têm comportamento real e legível. Mocks são úteis quando a interação em si importa, como confirmar que um evento foi publicado uma vez.
Testes de casos de uso e estado
Teste sucesso, validação, falha conhecida e exceção inesperada. Para controllers assíncronos, verifique a sequência de estados e controle o relógio, rede e armazenamento.
test('publica loading e dados ao carregar produtos', () async {
final repository = FakeProductRepository(products: sampleProducts);
final controller = ProductsController(GetProducts(repository));
final states = <ProductsState>[];
controller.addListener(() => states.add(controller.state));
await controller.load();
expect(states.first.isLoading, isTrue);
expect(states.last.items, sampleProducts);
});
Widget tests
Teste o que a pessoa percebe: texto, ação habilitada, validação e mudança de estado.
testWidgets('envia formulário válido', (tester) async {
await tester.pumpWidget(buildTestApp(const ProductFormPage()));
await tester.enterText(find.byKey(const Key('product-name')), 'Notebook');
await tester.tap(find.text('Salvar'));
await tester.pump();
expect(find.text('Produto salvo'), findsOneWidget);
});
Use keys estáveis somente quando semântica ou texto não forem suficientes. Evite snapshots que quebram por qualquer pixel sem proteger um requisito real.
Integração e jornadas críticas
Automatize poucos fluxos completos: login, catálogo, criação de pedido e logout. Execute contra ambiente controlado, com dados isolados e limpeza determinística.
Análise estática e cobertura
No pipeline, rode:
dart format --output=none --set-exit-if-changed .
flutter analyze
flutter test --coverage
dart format --output=none --set-exit-if-changed .Verifica formatação sem modificar o checkout. --output=none evita reescrita e --set-exit-if-changed transforma divergência em código de saída não zero. Isso torna a regra verificável no CI.
Se falhar, execute dart format . localmente, revise o diff e repita o gate.
flutter analyzeCarrega o contexto Flutter e executa o analisador Dart com as regras do projeto. O pipeline deve preservar arquivo, linha e código do diagnóstico para que a falha seja acionável.
Não use exclusões genéricas para esconder warnings recém-introduzidos. Quando uma exceção for legítima, documente-a no menor escopo possível.
flutter test --coverageExecuta a suíte e grava dados de cobertura, normalmente em coverage/lcov.info. O arquivo precisa ser interpretado por uma ferramenta de relatório; o número sozinho não avalia a força das asserções.
Separe falha de teste de falha na geração/publicação do relatório. A primeira invalida comportamento; a segunda reduz visibilidade e também deve ser tratada.
Cobertura é um sinal, não uma meta isolada. Priorize regras críticas, erros e transições. Cem por cento de linhas executadas pode esconder asserções fracas.
Pense no gate como feedback em camadas: formatação é rápida e determinística; análise encontra problemas estruturais; testes unitários cobrem regras; widgets cobrem interação; jornadas integradas cobrem poucos caminhos críticos. Quanto mais alto o nível, maior o custo e menor deve ser a quantidade.
Observabilidade sem exposição
Logs devem permitir reconstruir falhas sem registrar tokens, senhas, payloads pessoais ou dados financeiros. Prefira eventos estruturados com severidade, operação, duração, resultado e correlation ID.
Crash reporting precisa de ambientes separados, versão do app e símbolos de compilação. Eventos de negócio - login concluído, pedido criado, sincronização falhou - complementam exceções técnicas.
Monitore estabilidade, tempo de inicialização, falhas de API, abandono de fluxos e versões afetadas. Defina quem responde a alertas; telemetria sem processo vira ruído caro.
Prática guiada: transforme qualidade em evidência
Continue no Sales Management com os testes dos módulos anteriores passando. Nesta prática você não tentará “testar tudo”: vai proteger a regra de domínio, a transição assíncrona, a tela percebida pelo usuário e uma jornada crítica.
Checkpoint 1 — escreva a matriz antes do código
Crie docs/test-strategy.md:
| Risco | Camada | Caso mínimo |
|---|---|---|
| produto inválido entra no domínio | unitário | nome vazio é rejeitado |
| carregamento termina em estado incorreto | controller | loading → data e loading → error |
| falha não oferece nova tentativa | widget | erro exibe botão Tentar novamente |
| fluxo principal quebra entre camadas | integração | abrir catálogo, cadastrar e reencontrar produto |
Essa matriz é o contrato da suíte. Quantidade de testes e percentual de cobertura não substituem riscos nomeados.
Checkpoint 2 — proteja a regra mais barata primeiro
Em test/features/products/domain/product_test.dart:
import 'package:flutter_test/flutter_test.dart';
import 'package:sales_management/features/products/domain/product.dart';
void main() {
group('Product.create', () {
test('rejeita nome vazio', () {
expect(
() => Product.create(id: 'p-1', name: ' ', priceInCents: 100),
throwsA(isA<ArgumentError>()),
);
});
test('normaliza o nome válido', () {
final product = Product.create(
id: 'p-1',
name: ' Teclado ',
priceInCents: 15990,
);
expect(product.name, 'Teclado');
});
});
}
Execute somente esse arquivo até ficar verde:
flutter test test/features/products/domain/product_test.dart
Checkpoint 3 — controle a conclusão assíncrona
Use um Completer no fake para provar o estado intermediário, não apenas o resultado final:
test('publica loading antes dos produtos', () async {
final completer = Completer<List<Product>>();
final repository = ControlledProductRepository(completer.future);
final controller = ProductsController(GetProducts(repository));
final load = controller.load();
expect(controller.state, isA<ProductsLoading>());
completer.complete([sampleProduct]);
await load;
expect(controller.state, isA<ProductsData>());
});
Crie também o caso em que o future termina com erro. Não use Future.delayed: o teste ficaria lento e dependente de tempo real.
Checkpoint 4 — teste o que a pessoa consegue observar
Em test/features/products/presentation/products_page_test.dart, monte a página com dependências falsas e verifique os três estados:
testWidgets('oferece nova tentativa depois de falhar', (tester) async {
final controller = FakeProductsController.error();
await tester.pumpWidget(buildTestApp(
ProductsPage(controller: controller),
));
expect(find.text('Não foi possível carregar'), findsOneWidget);
await tester.tap(find.text('Tentar novamente'));
expect(controller.loadCalls, 1);
});
Acrescente casos para indicador de carregamento, catálogo preenchido e validação do formulário. Prefira texto e semântica; use Key apenas quando a interface não oferece um seletor estável.
Checkpoint 5 — automatize uma jornada crítica
Adicione a ferramenta oficial e crie integration_test/catalog_journey_test.dart:
flutter pub add --dev integration_test --sdk=flutter
flutter test integration_test/catalog_journey_test.dart
A jornada abre o catálogo, toca em Novo produto, preenche campos, salva e confirma que o item aparece. Execute com fake determinístico ou emuladores isolados. Em um dispositivo, o comando pode exigir um alvo conectado; registre essa dependência separadamente no README.
Checkpoint 6 — registre eventos sem registrar segredos
Crie lib/core/observability/app_logger.dart:
abstract interface class AppLogger {
void info(String event, Map<String, Object?> fields);
}
final class SafeLogger implements AppLogger {
SafeLogger(this._write);
final void Function(Map<String, Object?> event) _write;
static const _sensitive = {'password', 'token', 'authorization'};
@override
void info(String event, Map<String, Object?> fields) {
_write({
'event': event,
for (final entry in fields.entries)
entry.key: _sensitive.contains(entry.key.toLowerCase())
? '[REDACTED]'
: entry.value,
});
}
}
Teste a redação. Em seguida registre somente evento, duração, resultado, versão e correlation ID. Desative crash reporting nos testes e identifique o ambiente antes de enviar qualquer telemetria.
Checkpoint 7 — execute o gate completo
dart format --output=none --set-exit-if-changed .
flutter analyze
flutter test --coverage
Abra coverage/lcov.info com uma ferramenta de relatório e procure regras e ramos críticos sem teste. Não aumente a cobertura com asserções vazias.
Falhas comuns:
| Sintoma | Causa provável | Correção |
|---|---|---|
teste trava em pumpAndSettle | animação ou stream nunca estabiliza | avance frames específicos |
| teste assíncrono oscila | relógio/rede reais | injete clock, fake ou emulator |
| widget não encontra texto | localização ou estado incorreto | monte locale e dependências explicitamente |
| log ainda expõe segredo | redação baseada só no nome de um campo | centralize política e teste variações |
Critério de conclusão e prática independente
O módulo termina quando os quatro riscos da matriz possuem evidência, o gate passa e uma regressão intencional faz o teste correto falhar. Reverta a regressão depois da comprovação.
Como prática independente, adicione uma falha de persistência ao cadastro. A tela deve preservar os dados digitados, explicar o problema e permitir nova tentativa; proteja tudo com teste de controller e de widget.
Termos para revisar: test double, fake, mock, deterministic test, widget test, integration test, coverage, structured log, trace e metric. Consulte a referência do curso.