Dart: Dart Testing — From Unit Tests to Integration Tests
Last updated: 2026-08-26
Code without tests is a ticking time bomb — testing is an insurance policy for your future self.
1. What You Will Learn
- The test package: test() / group() / expect() with Matchers
- Unit testing: Isolated testing of functions and classes
- Integration testing: Verifying collaboration between multiple modules
- Mock and fake: mockito / fake_async
- Bob's Scenario: Test coverage for DataPipeline core logic
2. A Developer's Real Story
(1) Pain Point: Silent Breakage After Refactoring
Charlie refactored the amount calculation logic in DataPipeline, changing the tax rate from a fixed value to a query based on region. After local testing passed and deployment, the refactoring unexpectedly affected the discount stacking calculation — the discount amount became negative, causing incorrect refund amount calculations for 5,000 orders, leading to a direct loss of 25,000 USD. If there had been automated tests, this bug would have been caught before deployment.
(2) The Solution: A Testing System
Dart's test package provides a comprehensive testing framework. group organizes the test structure, expect + Matchers provide rich assertions, and setUp/tearDown manage the test environment.
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));
});
});
> **Output:** Run locally in DartPad or via `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x, and results may vary slightly with SDK versions.
(3) Benefits
- Automatically run tests after refactoring, covering 200+ test cases in 5 minutes.
- Edge cases (negative amounts, empty lists, null values) are no longer missed.
- Test coverage increased from 0% to 85%, with a 90% reduction in regression bugs.
3. Test Package Fundamentals
(1) Core Syntax
▶ Example
> **Output:** Run locally in DartPad or via `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x, and results may vary slightly with SDK versions.
: Basic Test
// 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);
});
}
> **Output:** Run locally in DartPad or via `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x, and results may vary slightly with SDK versions.
▶ Example
> **Output:** Run locally in DartPad or via `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x, and results may vary slightly with SDK versions.
: Organizing Tests with 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));
});
});
}
> **Output:** Run locally in DartPad or via `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x, and results may vary slightly with SDK versions.
(2) Common Matchers
| Matcher | Meaning | Example |
|---|---|---|
equals(value) |
Equals | equals(5) |
greaterThan(n) |
Greater than | greaterThan(0) |
lessThan(n) |
Less than | lessThan(100) |
closeTo(num, delta) |
Approximately equal | closeTo(3.14, 0.01) |
contains(item) |
Contains | contains('key') |
isEmpty |
Is empty | isEmpty |
isNotEmpty |
Is not empty | isNotEmpty |
isNull |
Is null | isNull |
isNotNull |
Is not null | isNotNull |
isTrue / isFalse |
Boolean check | isTrue |
throwsException |
Throws an exception | throwsException |
isA``<Type>``() |
Type check | isA``<String>``() |
4. setUp / tearDown
▶ Example
> **Output:** Run locally in DartPad or via `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x, and results may vary slightly with SDK versions.
: Managing the Test Environment
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(() {
// Runs before each test
book = OrderBook();
});
tearDown(() {
// Runs after each test
// Cleanup if needed
});
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);
});
});
}
> **Output:** Run locally in DartPad or via `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x, and results may vary slightly with SDK versions.
5. Unit Testing
(1) Principles of Isolated Testing
▶ Example
> **Output:** Run locally in DartPad or via `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x, and results may vary slightly with SDK versions.
: Unit Testing Functions
import 'package:test/test.dart';
// Functions to test
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'));
});
});
}
> **Output:** Run locally in DartPad or via `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x, and results may vary slightly with SDK versions.
6. Mock and fake
▶ Example
> **Output:** Run locally in DartPad or via `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x, and results may vary slightly with SDK versions.
: Manual Fake
import 'package:test/test.dart';
// Interface to mock
abstract class DataSource {
Future<List``<String>``> fetchIds();
Future``<double>`` fetchAmount(String id);
}
// Manual fake implementation
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');
}
// Service under test
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));
});
});
}
> **Output:** Run locally in DartPad or via `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x, and results may vary slightly with SDK versions.
7. Bob's Scenario: DataPipeline Test Coverage
▶ Example
> **Output:** Run locally in DartPad or via `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x, and results may vary slightly with SDK versions.
: Core Logic Testing
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);
});
});
}
> **Output:** Run locally in DartPad or via `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x, and results may vary slightly with SDK versions.
8. Complete Example: DataPipeline Test Suite
// ============================================
// DataPipeline Test Suite
// Comprehensive testing with groups, setup, and fakes
// ============================================
import 'package:test/test.dart';
// Production code
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,
};
}
}
// Tests
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'));
});
});
}
> **Output:** Run locally in DartPad or via `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x, and results may vary slightly with SDK versions.
❓ FAQ
Q: Where should test files be placed? A: In the
test/folder at the project root, with filenames ending in_test.dart. Runningdart testautomatically discovers all tests.
Q: What's the difference between unit tests and integration tests? A: Unit tests isolate and test a single function/class without relying on external resources. Integration tests verify that multiple modules collaborate correctly and may involve files, databases, or networks.
Q: Can
setUpbe nested within groups? A: Yes. The outer group'ssetUpruns first, followed by the inner one's. This is suitable for sharing basic settings + custom configuration per group.
Q: Should I choose mockito or a manual fake? A: For simple interfaces, use a manual fake (less code, type-safe). For complex interfaces or when you need to verify call counts, use mockito. Dart recommends prioritizing manual fakes.
Q: How do I test asynchronous code? A: The test callback supports
async, so usetest('...', () async { ... })directly.expectalso supports Futures. Don't forget toawait.
Q: How do I check test coverage? A: Run
dart test --coverage=coverage, then use theformat_coveragetool from thecoveragepackage to generate a report.
Q: How do I test code that throws exceptions? A: Use
expect(() => someFunction(), throwsException)or more specific variants likethrowsFormatException,throwsArgumentError, etc.
📖 Summary
- The test package provides three core APIs: test/group/expect, paired with rich Matchers.
grouporganizes test structure;setUp/tearDownmanage the test environment.- Unit tests isolate a single function/class without relying on external resources.
- Fake implementations replace real dependencies, making tests fast and repeatable.
- DataPipeline test coverage: Order validation, ReportGenerator calculations, edge cases.
📝 Exercises
- Basic (Difficulty ⭐): Write 5 unit tests for a
formatUSD(double amount)function: normal amount, zero, negative, very large number, and decimal precision. - Intermediate (Difficulty ⭐⭐): Create a
FakeApiServiceimplementing theDataSourceinterface. Use it to test thegetTotalRevenue()andgetOrderById()methods ofOrderService. Include both success and failure scenarios. - Advanced (Difficulty ⭐⭐⭐): Write a complete test suite for DataPipeline's
ReportGenerator: usesetUpto share test data,grouptests by functionality, test edge cases (empty list, single item, all pending status), with a coverage target of ≥ 90%.