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á
- O pacote test: test() / group() / expect() com Matchers
- Testes unitários: Teste isolado de funções e classes
- Testes de integração: Verificando colaboração entre múltiplos módulos
- Mock e fake: mockito / fake_async
- Cenário do Bob: Cobertura de testes para a lógica central do DataPipeline
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.
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));
});
});
> **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
- Executar automaticamente testes após refatoração, cobrindo 200+ casos de teste em 5 minutos.
- Casos extremos (valores negativos, listas vazias, valores nulos) não são mais esquecidos.
- Cobertura de testes aumentou de 0% para 85%, com uma redução de 90% em bugs de regressão.
3. Fundamentos do Pacote test
(1) Sintaxe Central
▶ Exemplo
> **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
// 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);
});
}
> **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
> **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
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));
});
});
}
> **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
> **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
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);
});
});
}
> **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
> **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
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'));
});
});
}
> **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
> **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
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));
});
});
}
> **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
> **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
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);
});
});
}
> **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
// ============================================
// 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'));
});
});
}
> **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. Executardart testdescobre 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:
setUppode ser aninhado dentro de groups? R: Sim. OsetUpdo 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 usetest('...', () async { ... })diretamente.expecttambém suporta Futures. Não se esqueça de usarawait.
P: Como verifico a cobertura de testes? R: Execute
dart test --coverage=coverage, depois use a ferramentaformat_coveragedo pacotecoveragepara gerar um relatório.
P: Como testo código que lança exceções? R: Use
expect(() => someFunction(), throwsException)ou variantes mais específicas comothrowsFormatException,throwsArgumentError, etc.
📖 Resumo
- O pacote test fornece três APIs centrais: test/group/expect, emparelhadas com Matchers ricos.
grouporganiza a estrutura de testes;setUp/tearDowngerenciam o ambiente de teste.- Testes unitários isolam uma única função/classe sem depender de recursos externos.
- Implementações fake substituem dependências reais, tornando os testes rápidos e repetíveis.
- Cobertura de testes do DataPipeline: validação de Order, cálculos do ReportGenerator, casos extremos.
📝 Exercícios
- 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. - Intermediário (Dificuldade ⭐⭐): Crie um
FakeApiServiceimplementando a interfaceDataSource. Use-o para testar os métodosgetTotalRevenue()egetOrderById()doOrderService. Inclua cenários de sucesso e falha. - Avançado (Dificuldade ⭐⭐⭐): Escreva uma suite de testes completa para o
ReportGeneratordo DataPipeline: usesetUppara compartilhar dados de teste,grouptestes por funcionalidade, teste casos extremos (lista vazia, item único, todos com status pending), com meta de cobertura ≥ 90%.