Dart: Testes em Dart

Última atualização: 2026-08-26

Código sem testes é uma bomba-relógio — testar é uma apólice de seguro para o seu eu do futuro.

1. O que Você Aprenderá


2. A História Real de um Desenvolvedor

(1) O Problema: Quebra Silenciosa Após Refatoração

Charlie refatorou a lógica de cálculo de valores no DataPipeline, alterando a taxa de imposto de um valor fixo para uma consulta baseada em região. Após testes locais passarem e a implantação, a refatoração afetou inesperadamente o cálculo de descontos empilhados — o valor do desconto tornou-se negativo, causando cálculos incorretos de reembolso para 5.000 pedidos, resultando em uma perda direta de 25.000 USD. Se houvesse testes automatizados, esse bug teria sido detectado antes da implantação.

(2) A Solução: Um Sistema de Testes

O pacote test do Dart fornece um framework de testes abrangente. group organiza a estrutura de testes, expect + Matchers fornece asserções ricas, e setUp/tearDown gerenciam o ambiente de teste.

DART
group('Order calculation', () {
  test('applies tax correctly', () {
    final order = Order(id: 'T1', amount: 1000.0);
    expect(order.calcTax(0.08), equals(80.0));
  });

  test('discount does not make total negative', () {
    final order = Order(id: 'T2', amount: 100.0, discountRate: 0.5);
    expect(order.total, greaterThan(0));
  });
});
TEXT 📖 Somente leitura
> **Saída:** Execute localmente no DartPad ou via `dart run`. Todos os exemplos do curso Dart são baseados em Dart 3.x / Flutter 3.x, e os resultados podem variar ligeiramente com as versões do SDK.

(3) Benefícios


3. Fundamentos do Pacote test

(1) Sintaxe Central

▶ Exemplo

TEXT 📖 Somente leitura
> **Saída:** Execute localmente no DartPad ou via `dart run`. Todos os exemplos do curso Dart são baseados em Dart 3.x / Flutter 3.x, e os resultados podem variar ligeiramente com as versões do SDK.

: Teste Básico

DART
// test/order_test.dart
import 'package:test/test.dart';

void main() {
  test('simple addition', () {
    expect(2 + 3, equals(5));
  });

  test('string contains', () {
    expect('DataPipeline v1.0', contains('Pipeline'));
  });

  test('list is not empty', () {
    expect([1, 2, 3], isNotEmpty);
  });
}
TEXT 📖 Somente leitura
> **Saída:** Execute localmente no DartPad ou via `dart run`. Todos os exemplos do curso Dart são baseados em Dart 3.x / Flutter 3.x, e os resultados podem variar ligeiramente com as versões do SDK.

▶ Exemplo

TEXT 📖 Somente leitura
> **Saída:** Execute localmente no DartPad ou via `dart run`. Todos os exemplos do curso Dart são baseados em Dart 3.x / Flutter 3.x, e os resultados podem variar ligeiramente com as versões do SDK.

: Organizando Testes com group

DART
import 'package:test/test.dart';

double calculateTax(double amount, double rate) => amount * rate;
double calculateTotal(double amount, double taxRate, double discountRate) {
  final discounted = amount * (1 - discountRate);
  return discounted * (1 + taxRate);
}

void main() {
  group('Tax calculation', () {
    test('standard rate', () {
      expect(calculateTax(1000.0, 0.08), equals(80.0));
    });

    test('zero rate', () {
      expect(calculateTax(1000.0, 0.0), equals(0.0));
    });

    test('high rate', () {
      expect(calculateTax(1000.0, 0.25), equals(250.0));
    });
  });

  group('Total calculation', () {
    test('no discount', () {
      expect(calculateTotal(1000.0, 0.08, 0.0), equals(1080.0));
    });

    test('with discount', () {
      expect(calculateTotal(1000.0, 0.08, 0.1), closeTo(972.0, 0.01));
    });

    test('full discount', () {
      expect(calculateTotal(1000.0, 0.08, 1.0), equals(0.0));
    });
  });
}
TEXT 📖 Somente leitura
> **Saída:** Execute localmente no DartPad ou via `dart run`. Todos os exemplos do curso Dart são baseados em Dart 3.x / Flutter 3.x, e os resultados podem variar ligeiramente com as versões do SDK.

(2) Matchers Comuns

Matcher Significado Exemplo
equals(value) Igual a equals(5)
greaterThan(n) Maior que greaterThan(0)
lessThan(n) Menor que lessThan(100)
closeTo(num, delta) Aproximadamente igual closeTo(3.14, 0.01)
contains(item) Contém contains('key')
isEmpty Está vazio isEmpty
isNotEmpty Não está vazio isNotEmpty
isNull É nulo isNull
isNotNull Não é nulo isNotNull
isTrue / isFalse Verificação booleana isTrue
throwsException Lança exceção throwsException
isA``<Type>``() Verificação de tipo isA``<String>``()

4. setUp / tearDown

▶ Exemplo

TEXT 📖 Somente leitura
> **Saída:** Execute localmente no DartPad ou via `dart run`. Todos os exemplos do curso Dart são baseados em Dart 3.x / Flutter 3.x, e os resultados podem variar ligeiramente com as versões do SDK.

: Gerenciando o Ambiente de Teste

DART
import 'package:test/test.dart';

class OrderBook {
  final List``<double>`` orders = [];

  void add(double amount) {
    if (amount <= 0) throw ArgumentError('Amount must be positive');
    orders.add(amount);
  }

  double get total => orders.fold(0.0, (a, b) => a + b);
  double get average => orders.isEmpty ? 0 : total / orders.length;
  int get count => orders.length;
}

void main() {
  late OrderBook book;

  setUp(() {
    // Executa antes de cada teste
    book = OrderBook();
  });

  tearDown(() {
    // Executa após cada teste
    // Limpeza se necessário
  });

  group('OrderBook', () {
    test('starts empty', () {
      expect(book.count, equals(0));
      expect(book.total, equals(0.0));
    });

    test('add increases count', () {
      book.add(100.0);
      expect(book.count, equals(1));
      book.add(200.0);
      expect(book.count, equals(2));
    });

    test('total sums amounts', () {
      book.add(1500.0);
      book.add(3200.0);
      expect(book.total, equals(4700.0));
    });

    test('rejects negative amount', () {
      expect(() => book.add(-50.0), throwsArgumentError);
    });
  });
}
TEXT 📖 Somente leitura
> **Saída:** Execute localmente no DartPad ou via `dart run`. Todos os exemplos do curso Dart são baseados em Dart 3.x / Flutter 3.x, e os resultados podem variar ligeiramente com as versões do SDK.

5. Testes Unitários

(1) Princípios de Teste Isolado

▶ Exemplo

TEXT 📖 Somente leitura
> **Saída:** Execute localmente no DartPad ou via `dart run`. Todos os exemplos do curso Dart são baseados em Dart 3.x / Flutter 3.x, e os resultados podem variar ligeiramente com as versões do SDK.

: Testes Unitários de Funções

DART
import 'package:test/test.dart';

// Funções para testar
double parseAmount(String input) {
  final value = double.tryParse(input);
  if (value == null) throw FormatException('Invalid amount: $input');
  if (value <= 0) throw ArgumentError('Amount must be positive');
  return value;
}

String formatUSD(double amount) => '\$${amount.toStringAsFixed(2)} USD';

String classifyAmount(double amount) => switch (amount) {
  >= 10000 => 'Enterprise',
  >= 1000 => 'Premium',
  > 0 => 'Standard',
  _ => throw ArgumentError('Invalid amount'),
};

void main() {
  group('parseAmount', () {
    test('parses valid integer', () {
      expect(parseAmount('1500'), equals(1500.0));
    });

    test('parses valid decimal', () {
      expect(parseAmount('99.99'), closeTo(99.99, 0.001));
    });

    test('throws on invalid input', () {
      expect(() => parseAmount('abc'), throwsFormatException);
    });

    test('throws on negative', () {
      expect(() => parseAmount('-50'), throwsArgumentError);
    });

    test('throws on zero', () {
      expect(() => parseAmount('0'), throwsArgumentError);
    });
  });

  group('formatUSD', () {
    test('formats whole number', () {
      expect(formatUSD(1500), equals('\$1500.00 USD'));
    });

    test('formats decimal', () {
      expect(formatUSD(99.99), equals('\$99.99 USD'));
    });
  });

  group('classifyAmount', () {
    test('Enterprise tier', () {
      expect(classifyAmount(15000), equals('Enterprise'));
    });

    test('Premium tier', () {
      expect(classifyAmount(1500), equals('Premium'));
    });

    test('Standard tier', () {
      expect(classifyAmount(50), equals('Standard'));
    });
  });
}
TEXT 📖 Somente leitura
> **Saída:** Execute localmente no DartPad ou via `dart run`. Todos os exemplos do curso Dart são baseados em Dart 3.x / Flutter 3.x, e os resultados podem variar ligeiramente com as versões do SDK.

6. Mock e fake

▶ Exemplo

TEXT 📖 Somente leitura
> **Saída:** Execute localmente no DartPad ou via `dart run`. Todos os exemplos do curso Dart são baseados em Dart 3.x / Flutter 3.x, e os resultados podem variar ligeiramente com as versões do SDK.

: Fake Manual

DART
import 'package:test/test.dart';

// Interface para mock
abstract class DataSource {
  Future<List``<String>``> fetchIds();
  Future``<double>`` fetchAmount(String id);
}

// Implementação fake manual
class FakeDataSource implements DataSource {
  final List``<String>`` _ids;
  final Map<String, double> _amounts;

  FakeDataSource({List``<String>``? ids, Map<String, double>? amounts})
      : _ids = ids ?? ['ORD-001', 'ORD-002'],
        _amounts = amounts ?? {'ORD-001': 1500.0, 'ORD-002': 3200.0};

  @override
  Future<List``<String>``> fetchIds() async => _ids;

  @override
  Future``<double>`` fetchAmount(String id) async =>
      _amounts[id] ?? throw Exception('Not found: $id');
}

// Serviço em teste
class OrderService {
  final DataSource _source;

  OrderService(this._source);

  Future``<double>`` getTotalRevenue() async {
    final ids = await _source.fetchIds();
    double total = 0;
    for (final id in ids) {
      total += await _source.fetchAmount(id);
    }
    return total;
  }
}

void main() {
  group('OrderService', () {
    test('calculates total revenue', () async {
      final fakeSource = FakeDataSource(
        ids: ['ORD-001', 'ORD-002'],
        amounts: {'ORD-001': 1500.0, 'ORD-002': 3200.0},
      );
      final service = OrderService(fakeSource);

      expect(await service.getTotalRevenue(), equals(4700.0));
    });

    test('handles empty data source', () async {
      final fakeSource = FakeDataSource(ids: [], amounts: {});
      final service = OrderService(fakeSource);

      expect(await service.getTotalRevenue(), equals(0.0));
    });
  });
}
TEXT 📖 Somente leitura
> **Saída:** Execute localmente no DartPad ou via `dart run`. Todos os exemplos do curso Dart são baseados em Dart 3.x / Flutter 3.x, e os resultados podem variar ligeiramente com as versões do SDK.

7. Cenário do Bob: Cobertura de Testes do DataPipeline

▶ Exemplo

TEXT 📖 Somente leitura
> **Saída:** Execute localmente no DartPad ou via `dart run`. Todos os exemplos do curso Dart são baseados em Dart 3.x / Flutter 3.x, e os resultados podem variar ligeiramente com as versões do SDK.

: Teste da Lógica Central

DART
import 'package:test/test.dart';

class Order {
  final String id;
  final double amount;
  final String status;
  final String category;
  final double? discountRate;

  Order({
    required this.id,
    required this.amount,
    required this.status,
    required this.category,
    this.discountRate,
  }) {
    if (amount <= 0) throw ArgumentError('Amount must be positive: $amount');
    if (id.isEmpty) throw ArgumentError('ID cannot be empty');
  }

  double get effectiveDiscount => discountRate ?? 0;
  double get discountedAmount => amount * (1 - effectiveDiscount);
  double get tax => discountedAmount * 0.08;
  double get total => discountedAmount + tax;
}

class OrderAnalyzer {
  static double totalRevenue(List``<Order>`` orders) =>
      orders.fold(0.0, (sum, o) => sum + o.total);

  static Map<String, double> revenueByCategory(List``<Order>`` orders) {
    final result = <String, double>{};
    for (final o in orders) {
      result.update(o.category, (v) => v + o.total, ifAbsent: () => o.total);
    }
    return result;
  }

  static List``<Order>`` filterCompleted(List``<Order>`` orders) =>
      orders.where((o) => o.status == 'completed').toList();
}

void main() {
  group('Order', () {
    test('calculates tax correctly', () {
      final order = Order(id: 'T1', amount: 1000.0, status: 'completed', category: 'E');
      expect(order.tax, closeTo(80.0, 0.01));
    });

    test('calculates total with discount', () {
      final order = Order(id: 'T2', amount: 1000.0, status: 'completed', category: 'E', discountRate: 0.1);
      expect(order.total, closeTo(972.0, 0.01));
    });

    test('rejects negative amount', () {
      expect(() => Order(id: 'T3', amount: -100, status: 'completed', category: 'E'),
          throwsArgumentError);
    });

    test('rejects empty ID', () {
      expect(() => Order(id: '', amount: 100, status: 'completed', category: 'E'),
          throwsArgumentError);
    });
  });

  group('OrderAnalyzer', () {
    late List``<Order>`` testOrders;

    setUp(() {
      testOrders = [
        Order(id: 'ORD-001', amount: 1500.0, status: 'completed', category: 'Electronics'),
        Order(id: 'ORD-002', amount: 3200.0, status: 'pending', category: 'Electronics'),
        Order(id: 'ORD-003', amount: 890.0, status: 'completed', category: 'Clothing'),
      ];
    });

    test('filters completed orders', () {
      final completed = OrderAnalyzer.filterCompleted(testOrders);
      expect(completed.length, equals(2));
      expect(completed.every((o) => o.status == 'completed'), isTrue);
    });

    test('calculates total revenue', () {
      final revenue = OrderAnalyzer.totalRevenue(testOrders);
      expect(revenue, greaterThan(0));
    });

    test('groups revenue by category', () {
      final byCategory = OrderAnalyzer.revenueByCategory(testOrders);
      expect(byCategory.containsKey('Electronics'), isTrue);
      expect(byCategory.containsKey('Clothing'), isTrue);
    });

    test('handles empty list', () {
      expect(OrderAnalyzer.totalRevenue([]), equals(0.0));
      expect(OrderAnalyzer.filterCompleted([]), isEmpty);
      expect(OrderAnalyzer.revenueByCategory([]), isEmpty);
    });
  });
}
TEXT 📖 Somente leitura
> **Saída:** Execute localmente no DartPad ou via `dart run`. Todos os exemplos do curso Dart são baseados em Dart 3.x / Flutter 3.x, e os resultados podem variar ligeiramente com as versões do SDK.

8. Exemplo Completo: Suite de Testes do DataPipeline

DART
// ============================================
// DataPipeline Test Suite
// Testes abrangentes com groups, setup e fakes
// ============================================

import 'package:test/test.dart';

// Código de produção
class PipelineConfig {
  final int batchSize;
  final double taxRate;
  final String outputFormat;

  PipelineConfig({
    this.batchSize = 10000,
    this.taxRate = 0.08,
    this.outputFormat = 'json',
  });

  PipelineConfig copyWith({int? batchSize, double? taxRate, String? outputFormat}) =>
      PipelineConfig(
        batchSize: batchSize ?? this.batchSize,
        taxRate: taxRate ?? this.taxRate,
        outputFormat: outputFormat ?? this.outputFormat,
      );
}

class Order {
  final String id;
  final double amount;
  final String status;
  final String category;

  Order({
    required this.id,
    required this.amount,
    required this.status,
    required this.category,
  }) {
    if (id.isEmpty) throw ArgumentError('Empty order ID');
    if (amount <= 0) throw ArgumentError('Amount must be positive');
  }
}

class ReportGenerator {
  final PipelineConfig config;

  ReportGenerator(this.config);

  Map<String, dynamic> generate(List``<Order>`` orders) {
    final completed = orders.where((o) => o.status == 'completed').toList();
    final revenue = completed.fold``<double>``(0, (s, o) => s + o.amount);
    final tax = revenue * config.taxRate;

    final byCategory = <String, int>{};
    for (final o in completed) {
      byCategory.update(o.category, (v) => v + 1, ifAbsent: () => 1);
    }

    return {
      'totalOrders': orders.length,
      'completedOrders': completed.length,
      'revenue': revenue,
      'tax': tax,
      'totalWithTax': revenue + tax,
      'byCategory': byCategory,
      'format': config.outputFormat,
    };
  }
}

// Testes
void main() {
  group('PipelineConfig', () {
    test('has default values', () {
      final config = PipelineConfig();
      expect(config.batchSize, equals(10000));
      expect(config.taxRate, equals(0.08));
      expect(config.outputFormat, equals('json'));
    });

    test('copyWith preserves unspecified fields', () {
      final original = PipelineConfig(batchSize: 50000);
      final modified = original.copyWith(taxRate: 0.10);
      expect(modified.batchSize, equals(50000));
      expect(modified.taxRate, equals(0.10));
    });
  });

  group('Order', () {
    test('creates valid order', () {
      final order = Order(id: 'ORD-001', amount: 1500.0, status: 'completed', category: 'E');
      expect(order.id, equals('ORD-001'));
      expect(order.amount, equals(1500.0));
    });

    test('rejects empty ID', () {
      expect(() => Order(id: '', amount: 100, status: 'ok', category: 'E'),
          throwsArgumentError);
    });

    test('rejects non-positive amount', () {
      expect(() => Order(id: 'X', amount: 0, status: 'ok', category: 'E'),
          throwsArgumentError);
      expect(() => Order(id: 'X', amount: -1, status: 'ok', category: 'E'),
          throwsArgumentError);
    });
  });

  group('ReportGenerator', () {
    late PipelineConfig config;
    late ReportGenerator generator;
    late List``<Order>`` testOrders;

    setUp(() {
      config = PipelineConfig(taxRate: 0.08);
      generator = ReportGenerator(config);
      testOrders = [
        Order(id: 'ORD-001', amount: 1500.0, status: 'completed', category: 'Electronics'),
        Order(id: 'ORD-002', amount: 50.0, status: 'completed', category: 'Books'),
        Order(id: 'ORD-003', amount: 3200.0, status: 'pending', category: 'Electronics'),
      ];
    });

    test('counts total and completed orders', () {
      final report = generator.generate(testOrders);
      expect(report['totalOrders'], equals(3));
      expect(report['completedOrders'], equals(2));
    });

    test('calculates revenue from completed only', () {
      final report = generator.generate(testOrders);
      expect(report['revenue'], equals(1550.0));
    });

    test('applies tax rate from config', () {
      final report = generator.generate(testOrders);
      expect(report['tax'], closeTo(124.0, 0.01));
    });

    test('groups by category', () {
      final report = generator.generate(testOrders);
      final byCategory = report['byCategory'] as Map<String, int>;
      expect(byCategory['Electronics'], equals(1));
      expect(byCategory['Books'], equals(1));
    });

    test('handles empty order list', () {
      final report = generator.generate([]);
      expect(report['totalOrders'], equals(0));
      expect(report['revenue'], equals(0.0));
    });

    test('respects output format config', () {
      final jsonGen = ReportGenerator(PipelineConfig(outputFormat: 'json'));
      final csvGen = ReportGenerator(PipelineConfig(outputFormat: 'csv'));
      expect(jsonGen.generate(testOrders)['format'], equals('json'));
      expect(csvGen.generate(testOrders)['format'], equals('csv'));
    });
  });
}
TEXT 📖 Somente leitura
> **Saída:** Execute localmente no DartPad ou via `dart run`. Todos os exemplos do curso Dart são baseados em Dart 3.x / Flutter 3.x, e os resultados podem variar ligeiramente com as versões do SDK.

❓ Perguntas Frequentes

P: Onde os arquivos de teste devem ser colocados? R: Na pasta test/ na raiz do projeto, com nomes de arquivo terminando em _test.dart. Executar dart test descobre automaticamente todos os testes.

P: Qual a diferença entre testes unitários e testes de integração? R: Testes unitários isolam e testam uma única função/classe sem depender de recursos externos. Testes de integração verificam que múltiplos módulos colaboram corretamente e podem envolver arquivos, bancos de dados ou redes.

P: setUp pode ser aninhado dentro de groups? R: Sim. O setUp do grupo externo executa primeiro, seguido pelo do grupo interno. Isso é adequado para compartilhar configurações básicas + configuração personalizada por grupo.

P: Devo escolher mockito ou um fake manual? R: Para interfaces simples, use um fake manual (menos código, tipo-seguro). Para interfaces complexas ou quando precisar verificar contagens de chamadas, use mockito. Dart recomenda priorizar fakes manuais.

P: Como testo código assíncrono? R: O callback de teste suporta async, então use test('...', () async { ... }) diretamente. expect também suporta Futures. Não se esqueça de usar await.

P: Como verifico a cobertura de testes? R: Execute dart test --coverage=coverage, depois use a ferramenta format_coverage do pacote coverage para gerar um relatório.

P: Como testo código que lança exceções? R: Use expect(() => someFunction(), throwsException) ou variantes mais específicas como throwsFormatException, throwsArgumentError, etc.


📖 Resumo


📝 Exercícios

  1. Básico (Dificuldade ⭐): Escreva 5 testes unitários para uma função formatUSD(double amount): valor normal, zero, negativo, número muito grande e precisão decimal.
  2. Intermediário (Dificuldade ⭐⭐): Crie um FakeApiService implementando a interface DataSource. Use-o para testar os métodos getTotalRevenue() e getOrderById() do OrderService. Inclua cenários de sucesso e falha.
  3. Avançado (Dificuldade ⭐⭐⭐): Escreva uma suite de testes completa para o ReportGenerator do DataPipeline: use setUp para compartilhar dados de teste, group testes por funcionalidade, teste casos extremos (lista vazia, item único, todos com status pending), com meta de cobertura ≥ 90%.

← Lição Anterior | Próxima Lição →

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%