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

CI flutter analyze

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

CI flutter test --coverage

Executa 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:

RiscoCamadaCaso mínimo
produto inválido entra no domíniounitárionome vazio é rejeitado
carregamento termina em estado incorretocontrollerloading → data e loading → error
falha não oferece nova tentativawidgeterro exibe botão Tentar novamente
fluxo principal quebra entre camadasintegraçãoabrir 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:

SintomaCausa provávelCorreção
teste trava em pumpAndSettleanimação ou stream nunca estabilizaavance frames específicos
teste assíncrono oscilarelógio/rede reaisinjete clock, fake ou emulator
widget não encontra textolocalização ou estado incorretomonte locale e dependências explicitamente
log ainda expõe segredoredação baseada só no nome de um campocentralize 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.