Dart testing
Dart testing patterns with dart test package, including mocking with mockito, async testing, test coverage, TDD workflow, and integration testing strategiesFrom its SKILL.md
npx -y skills add FaisalAlqarni/sp-ecc --skill dart-testingAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 5 stars5 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
35.8 KB, as published. Nobody here has run it
Dart Testing Patterns
Comprehensive Dart testing patterns covering the dart test package, mockito for mocking, async testing, test coverage, and TDD methodology for writing maintainable, reliable tests.
When to Activate
- Writing new Dart classes or methods
- Adding test coverage to existing Dart code
- Setting up test frameworks for Dart projects
- Following TDD workflow in Dart applications
- Testing Flutter or server-side Dart applications
- Creating package tests for pub.dev
Dart Test Package Basics
Test Structure and Organization
// test/calculator_test.dart
import 'package:test/test.dart';
import 'package:my_package/calculator.dart';
void main() {
// group - organizes related tests
group('Calculator', () {
// test - defines a single test case
test('adds two numbers correctly', () {
final calculator = Calculator();
final result = calculator.add(2, 3);
expect(result, equals(5));
});
test('subtracts two numbers correctly', () {
final calculator = Calculator();
final result = calculator.subtract(5, 3);
expect(result, equals(2));
});
// Nested groups for better organization
group('multiply', () {
test('multiplies positive numbers', () {
final calculator = Calculator();
expect(calculator.multiply(3, 4), equals(12));
});
test('multiplies with zero', () {
final calculator = Calculator();
expect(calculator.multiply(5, 0), equals(0));
});
test('multiplies negative numbers', () {
final calculator = Calculator();
expect(calculator.multiply(-2, -3), equals(6));
});
});
});
}
setUp and tearDown Hooks
import 'package:test/test.dart';
void main() {
group('Database', () {
late Database database;
// setUp runs before each test
setUp(() {
database = Database.connect();
database.initialize();
});
// tearDown runs after each test
tearDown(() {
database.close();
});
test('inserts a record', () {
final result = database.insert('users', {'name': 'Alice'});
expect(result, isTrue);
});
test('queries records', () {
database.insert('users', {'name': 'Alice'});
final users = database.query('users');
expect(users, hasLength(1));
});
});
}
setUpAll and tearDownAll
void main() {
group('API Tests', () {
late HttpServer server;
late ApiClient client;
// setUpAll runs once before all tests in the group
setUpAll(() async {
server = await HttpServer.bind('localhost', 8080);
client = ApiClient('http://localhost:8080');
});
// tearDownAll runs once after all tests in the group
tearDownAll(() async {
await client.close();
await server.close();
});
// setUp/tearDown for per-test setup
setUp(() {
// Reset state before each test
client.clearCache();
});
test('fetches user data', () async {
final user = await client.getUser('1');
expect(user.name, equals('Alice'));
});
test('handles 404 errors', () async {
expect(
() => client.getUser('nonexistent'),
throwsA(isA<NotFoundException>()),
);
});
});
}
Test Directory Structure
my_package/
├── lib/
│ ├── my_package.dart
│ └── src/
│ ├── models/
│ │ ├── user.dart
│ │ └── product.dart
│ ├── services/
│ │ ├── api_service.dart
│ │ └── auth_service.dart
│ └── utils/
│ └── validators.dart
├── test/
│ ├── models/
│ │ ├── user_test.dart
│ │ └── product_test.dart
│ ├── services/
│ │ ├── api_service_test.dart
│ │ └── auth_service_test.dart
│ ├── utils/
│ │ └── validators_test.dart
│ ├── integration/
│ │ └── user_flow_test.dart
│ └── test_helpers.dart
└── pubspec.yaml
File Naming Conventions
// Source file: lib/src/services/user_service.dart
// Test file: test/services/user_service_test.dart
// Use _test.dart suffix for test files
// Mirror the lib/ directory structure in test/
// Test file example
// test/services/user_service_test.dart
import 'package:test/test.dart';
import 'package:my_package/src/services/user_service.dart';
void main() {
test('UserService processes users', () {
// Test implementation
});
}
Test Matchers
Core Matchers
import 'package:test/test.dart';
void main() {
group('Core Matchers', () {
test('equality matchers', () {
expect(2 + 2, equals(4));
expect([1, 2, 3], equals([1, 2, 3]));
expect({'a': 1}, equals({'a': 1}));
// same() checks object identity
final list = [1, 2, 3];
expect(list, same(list));
expect([1, 2, 3], isNot(same([1, 2, 3])));
});
test('boolean matchers', () {
expect(true, isTrue);
expect(false, isFalse);
expect(1 < 2, isTrue);
expect(null, isNull);
expect('text', isNotNull);
});
test('numeric matchers', () {
expect(5, greaterThan(3));
expect(5, greaterThanOrEqualTo(5));
expect(3, lessThan(5));
expect(3, lessThanOrEqualTo(3));
expect(3.14, closeTo(3.1, 0.1));
expect(10, isPositive);
expect(-5, isNegative);
expect(0, isZero);
expect(0, isNonZero);
});
test('string matchers', () {
expect('hello world', contains('world'));
expect('hello', startsWith('hel'));
expect('world', endsWith('rld'));
expect('[email protected]', matches(r'^\w+@\w+\.\w+$'));
expect('HELLO', equalsIgnoringCase('hello'));
expect(' text ', equalsIgnoringWhitespace('text'));
});
test('collection matchers', () {
expect([1, 2, 3], contains(2));
expect([1, 2, 3], containsAll([1, 3]));
expect([1, 2, 3], hasLength(3));
expect([], isEmpty);
expect([1], isNotEmpty);
// unorderedEquals - order doesn't matter
expect([3, 1, 2], unorderedEquals([1, 2, 3]));
// containsAllInOrder - subset in order
expect([1, 2, 3, 4], containsAllInOrder([1, 3, 4]));
// everyElement - all elements match
expect([2, 4, 6], everyElement(isEven));
// anyElement - at least one matches
expect([1, 2, 3], anyElement(greaterThan(2)));
});
test('type matchers', () {
expect(42, isA<int>());
expect('hello', isA<String>());
expect([1, 2], isA<List<int>>());
expect({'key': 'value'}, isA<Map<String, String>>());
});
test('map matchers', () {
final map = {'name': 'Alice', 'age': 30};
expect(map, containsPair('name', 'Alice'));
expect(map, containsValue(30));
});
test('combining matchers', () {
expect(5, allOf([greaterThan(3), lessThan(10)]));
expect(5, anyOf([equals(5), equals(6)]));
expect(5, isNot(equals(6)));
});
});
}
Custom Matchers
import 'package:test/test.dart';
// Custom matcher for even numbers
class IsEven extends Matcher {
@override
bool matches(item, Map matchState) {
return item is int && item % 2 == 0;
}
@override
Description describe(Description description) {
return description.add('an even number');
}
@override
Description describeMismatch(
item,
Description mismatchDescription,
Map matchState,
bool verbose,
) {
return mismatchDescription.add('$item is not even');
}
}
// Convenience function
Matcher isEven() => IsEven();
// Custom matcher for valid email
class IsValidEmail extends Matcher {
final _emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
@override
bool matches(item, Map matchState) {
return item is String && _emailRegex.hasMatch(item);
}
@override
Description describe(Description description) {
return description.add('a valid email address');
}
}
Matcher isValidEmail() => IsValidEmail();
// Usage
void main() {
test('custom matchers', () {
expect(4, isEven());
expect(7, isNot(isEven()));
expect('[email protected]', isValidEmail());
expect('invalid-email', isNot(isValidEmail()));
});
}
Exception and Error Testing
import 'package:test/test.dart';
void main() {
group('Exception Testing', () {
test('throws any exception', () {
expect(() => throw Exception('error'), throwsException);
});
test('throws specific exception type', () {
expect(() => throw FormatException(), throwsFormatException);
expect(() => throw ArgumentError(), throwsArgumentError);
expect(() => throw StateError('bad state'), throwsStateError);
expect(() => throw UnsupportedError('not supported'), throwsUnsupportedError);
});
test('throws with type matcher', () {
expect(() => throw CustomException('error'), throwsA(isA<CustomException>()));
});
test('throws with predicate', () {
expect(
() => throw ArgumentError('invalid value'),
throwsA(
isA<ArgumentError>().having(
(e) => e.message,
'message',
contains('invalid'),
),
),
);
});
test('throws with custom message check', () {
expect(
() => throw Exception('custom error'),
throwsA(
predicate((e) => e.toString().contains('custom error')),
),
);
});
test('async function throws', () async {
await expectLater(
() async => throw Exception('async error'),
throwsException,
);
});
});
}
class CustomException implements Exception {
final String message;
CustomException(this.message);
@override
String toString() => 'CustomException: $message';
}
Mocking with Mockito
Setting Up Mockito
# pubspec.yaml
dev_dependencies:
test: ^1.24.0
mockito: ^5.4.0
build_runner: ^2.4.0
Creating Mock Classes
// lib/src/services/user_service.dart
abstract class UserRepository {
Future<User> getUser(String id);
Future<void> saveUser(User user);
Future<List<User>> getAllUsers();
}
// test/services/user_service_test.dart
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:test/test.dart';
// Generate mocks with build_runner
@GenerateNiceMocks([MockSpec<UserRepository>()])
import 'user_service_test.mocks.dart';
void main() {
group('UserService', () {
late MockUserRepository mockRepository;
late UserService service;
setUp(() {
mockRepository = MockUserRepository();
service = UserService(mockRepository);
});
test('getUser calls repository', () async {
final user = User(id: '1', name: 'Alice');
// Stub method call
when(mockRepository.getUser('1'))
.thenAnswer((_) async => user);
final result = await service.getUser('1');
expect(result.name, equals('Alice'));
// Verify method was called
verify(mockRepository.getUser('1')).called(1);
});
test('saveUser validates before saving', () async {
final user = User(id: '1', name: '');
expect(
() => service.saveUser(user),
throwsA(isA<ValidationException>()),
);
// Verify repository was never called
verifyNever(mockRepository.saveUser(any));
});
});
}
// Run: dart run build_runner build
Mockito Argument Matchers
import 'package:mockito/mockito.dart';
import 'package:test/test.dart';
void main() {
group('Argument Matchers', () {
late MockUserRepository repository;
setUp(() {
repository = MockUserRepository();
});
test('any matcher', () async {
when(repository.getUser(any))
.thenAnswer((_) async => User(id: '1', name: 'Alice'));
final user = await repository.getUser('any-id');
expect(user.name, equals('Alice'));
verify(repository.getUser(any));
});
test('argThat with predicate', () async {
when(repository.getUser(argThat(startsWith('user-'))))
.thenAnswer((_) async => User(id: '1', name: 'Alice'));
final user = await repository.getUser('user-123');
expect(user.name, equals('Alice'));
verify(repository.getUser(argThat(startsWith('user-'))));
});
test('captureThat for inspecting arguments', () async {
when(repository.saveUser(any))
.thenAnswer((_) async => {});
final user = User(id: '1', name: 'Alice');
await repository.saveUser(user);
final captured = verify(repository.saveUser(captureAny)).captured;
expect(captured.single, same(user));
});
test('different return values for different arguments', () async {
when(repository.getUser('1'))
.thenAnswer((_) async => User(id: '1', name: 'Alice'));
when(repository.getUser('2'))
.thenAnswer((_) async => User(id: '2', name: 'Bob'));
final user1 = await repository.getUser('1');
final user2 = await repository.getUser('2');
expect(user1.name, equals('Alice'));
expect(user2.name, equals('Bob'));
});
test('throwing exceptions', () {
when(repository.getUser('invalid'))
.thenThrow(NotFoundException('User not found'));
expect(
() => repository.getUser('invalid'),
throwsA(isA<NotFoundException>()),
);
});
test('sequential return values', () async {
when(repository.getUser('1'))
.thenAnswer((_) async => User(id: '1', name: 'Alice'))
.thenAnswer((_) async => User(id: '1', name: 'Alice Updated'));
final user1 = await repository.getUser('1');
final user2 = await repository.getUser('1');
expect(user1.name, equals('Alice'));
expect(user2.name, equals('Alice Updated'));
});
});
}
Verification with Mockito
import 'package:mockito/mockito.dart';
import 'package:test/test.dart';
void main() {
group('Verification', () {
late MockUserRepository repository;
setUp(() {
repository = MockUserRepository();
});
test('verify called once', () async {
when(repository.getUser(any))
.thenAnswer((_) async => User(id: '1', name: 'Alice'));
await repository.getUser('1');
verify(repository.getUser('1')).called(1);
});
test('verify called multiple times', () async {
when(repository.getUser(any))
.thenAnswer((_) async => User(id: '1', name: 'Alice'));
await repository.getUser('1');
await repository.getUser('1');
await repository.getUser('1');
verify(repository.getUser('1')).called(3);
});
test('verify never called', () {
verifyNever(repository.getUser(any));
});
test('verify call order', () async {
when(repository.getUser(any))
.thenAnswer((_) async => User(id: '1', name: 'Alice'));
when(repository.saveUser(any))
.thenAnswer((_) async => {});
await repository.getUser('1');
await repository.saveUser(User(id: '1', name: 'Alice'));
verifyInOrder([
repository.getUser('1'),
repository.saveUser(any),
]);
});
test('verifyNoMoreInteractions', () async {
when(repository.getUser('1'))
.thenAnswer((_) async => User(id: '1', name: 'Alice'));
await repository.getUser('1');
verify(repository.getUser('1'));
verifyNoMoreInteractions(repository);
});
test('untilCalled for async waiting', () async {
// Start async operation
Future.delayed(Duration(milliseconds: 100), () {
repository.getUser('1');
});
// Wait until method is called
await untilCalled(repository.getUser('1'));
verify(repository.getUser('1')).called(1);
});
});
}
Async Testing Patterns
Testing Futures
import 'package:test/test.dart';
void main() {
group('Future Testing', () {
test('async function returns correct value', () async {
final result = await fetchData();
expect(result, equals('data'));
});
test('async function with expectLater', () {
expectLater(fetchData(), completion(equals('data')));
});
test('async function throws', () async {
await expectLater(
fetchDataWithError(),
throwsA(isA<Exception>()),
);
});
test('future completes successfully', () {
expect(Future.value(42), completion(equals(42)));
});
test('future throws exception', () {
expect(
Future.error(Exception('error')),
throwsException,
);
});
test('multiple async operations', () async {
final results = await Future.wait([
fetchData(),
fetchData(),
fetchData(),
]);
expect(results, hasLength(3));
expect(results, everyElement(equals('data')));
});
test('async with timeout', () async {
await expectLater(
slowOperation().timeout(Duration(milliseconds: 100)),
throwsA(isA<TimeoutException>()),
);
});
});
}
Future<String> fetchData() async {
await Future.delayed(Duration(milliseconds: 10));
return 'data';
}
Future<void> fetchDataWithError() async {
await Future.delayed(Duration(milliseconds: 10));
throw Exception('error');
}
Future<void> slowOperation() async {
await Future.delayed(Duration(seconds: 1));
}
Testing Streams
import 'package:test/test.dart';
void main() {
group('Stream Testing', () {
test('stream emits expected values', () {
final stream = Stream.fromIterable([1, 2, 3]);
expect(stream, emitsInOrder([1, 2, 3]));
});
test('stream emits and completes', () {
final stream = Stream.fromIterable([1, 2, 3]);
expect(
stream,
emitsInOrder([
1,
2,
3,
emitsDone,
]),
);
});
test('stream emits error', () {
final stream = Stream.error(Exception('error'));
expect(stream, emitsError(isA<Exception>()));
});
test('async stream processing', () async {
final stream = countStream(3);
final values = <int>[];
await for (final value in stream) {
values.add(value);
}
expect(values, equals([1, 2, 3]));
});
test('expectLater with streams', () async {
final stream = countStream(3);
await expectLater(
stream,
emitsInOrder([1, 2, 3, emitsDone]),
);
});
test('stream emits any of values', () {
final stream = Stream.fromIterable([1, 2, 3]);
expect(stream, emits(anyOf([1, 2, 3])));
});
test('stream with transformations', () {
final stream = Stream.fromIterable([1, 2, 3])
.map((n) => n * 2);
expect(stream, emitsInOrder([2, 4, 6]));
});
test('broadcast stream with multiple listeners', () {
final controller = StreamController<int>.broadcast();
final stream = controller.stream;
expect(stream, emitsInOrder([1, 2, 3]));
expect(stream, emitsInOrder([1, 2, 3]));
controller.add(1);
controller.add(2);
controller.add(3);
controller.close();
});
test('stream controller lifecycle', () async {
final controller = StreamController<int>();
final future = expectLater(
controller.stream,
emitsInOrder([1, 2, 3, emitsDone]),
);
controller.add(1);
controller.add(2);
controller.add(3);
await controller.close();
await future;
});
});
}
Stream<int> countStream(int max) async* {
for (int i = 1; i <= max; i++) {
await Future.delayed(Duration(milliseconds: 10));
yield i;
}
}
Testing with FakeAsync
import 'package:test/test.dart';
import 'package:fake_async/fake_async.dart';
void main() {
group('FakeAsync Testing', () {
test('controls time progression', () {
fakeAsync((async) {
var callbackExecuted = false;
Future.delayed(Duration(seconds: 5), () {
callbackExecuted = true;
});
// Time hasn't elapsed yet
expect(callbackExecuted, isFalse);
// Advance time by 5 seconds
async.elapse(Duration(seconds: 5));
// Now callback has executed
expect(callbackExecuted, isTrue);
});
});
test('tests periodic timers', () {
fakeAsync((async) {
var count = 0;
Timer.periodic(Duration(seconds: 1), (timer) {
count++;
if (count >= 3) timer.cancel();
});
expect(count, equals(0));
async.elapse(Duration(seconds: 1));
expect(count, equals(1));
async.elapse(Duration(seconds: 1));
expect(count, equals(2));
async.elapse(Duration(seconds: 1));
expect(count, equals(3));
});
});
test('flushMicrotasks', () {
fakeAsync((async) {
var executed = false;
scheduleMicrotask(() {
executed = true;
});
expect(executed, isFalse);
async.flushMicrotasks();
expect(executed, isTrue);
});
});
test('flushTimers', () {
fakeAsync((async) {
var count = 0;
Timer(Duration(seconds: 1), () => count++);
Timer(Duration(seconds: 2), () => count++);
Timer(Duration(seconds: 3), () => count++);
expect(count, equals(0));
async.flushTimers();
expect(count, equals(3));
});
});
});
}
Test Coverage
Running Coverage
# Install coverage package
dart pub global activate coverage
# Run tests with coverage
dart run coverage:test_with_coverage
# Generate HTML report
genhtml -o coverage/html coverage/lcov.info
# Open coverage report
open coverage/html/index.html
Coverage Configuration
# dart_test.yaml
coverage:
enabled: true
# Exclude generated files
exclude:
- '**/*.g.dart'
- '**/*.freezed.dart'
- 'lib/generated/**'
# Set minimum coverage threshold
minimum: 80
Coverage Expectations
| Code Type | Target Coverage |
|---|---|
| Critical business logic | 100% |
| Models and data classes | 90%+ |
| Services and repositories | 90%+ |
| Utilities and helpers | 85%+ |
| UI code (Flutter widgets) | 70%+ |
| Generated code | Excluded |
Interpreting Coverage Results
// Good coverage - all paths tested
int divide(int a, int b) {
if (b == 0) {
throw ArgumentError('Cannot divide by zero');
}
return a ~/ b;
}
// Test with full coverage
test('divide with coverage', () {
expect(divide(10, 2), equals(5));
expect(() => divide(10, 0), throwsArgumentError);
});
// Poor coverage - missing error case
test('divide without full coverage', () {
expect(divide(10, 2), equals(5));
// Missing: divide by zero case
});
Integration Testing
API Integration Tests
// test/integration/api_integration_test.dart
import 'package:test/test.dart';
import 'package:http/http.dart' as http;
void main() {
group('API Integration Tests', () {
late http.Client client;
late String baseUrl;
setUpAll(() {
client = http.Client();
baseUrl = 'http://localhost:8080';
});
tearDownAll(() {
client.close();
});
test('full user workflow', () async {
// Create user
final createResponse = await client.post(
Uri.parse('$baseUrl/users'),
body: {'name': 'Alice', 'email': '[email protected]'},
);
expect(createResponse.statusCode, equals(201));
final userId = jsonDecode(createResponse.body)['id'];
// Get user
final getResponse = await client.get(
Uri.parse('$baseUrl/users/$userId'),
);
expect(getResponse.statusCode, equals(200));
final user = jsonDecode(getResponse.body);
expect(user['name'], equals('Alice'));
// Update user
final updateResponse = await client.put(
Uri.parse('$baseUrl/users/$userId'),
body: {'name': 'Alice Updated'},
);
expect(updateResponse.statusCode, equals(200));
// Delete user
final deleteResponse = await client.delete(
Uri.parse('$baseUrl/users/$userId'),
);
expect(deleteResponse.statusCode, equals(204));
// Verify deletion
final verifyResponse = await client.get(
Uri.parse('$baseUrl/users/$userId'),
);
expect(verifyResponse.statusCode, equals(404));
});
});
}
Database Integration Tests
// test/integration/database_integration_test.dart
import 'package:test/test.dart';
void main() {
group('Database Integration', () {
late Database database;
setUpAll(() async {
database = await Database.connect('test_db');
await database.migrate();
});
tearDownAll(() async {
await database.dropAllTables();
await database.close();
});
setUp(() async {
await database.clearAllData();
});
test('user CRUD operations', () async {
// Create
final user = User(name: 'Alice', email: '[email protected]');
final savedUser = await database.users.insert(user);
expect(savedUser.id, isNotNull);
// Read
final fetchedUser = await database.users.findById(savedUser.id);
expect(fetchedUser?.name, equals('Alice'));
// Update
final updatedUser = fetchedUser!.copyWith(name: 'Alice Updated');
await database.users.update(updatedUser);
final refetchedUser = await database.users.findById(savedUser.id);
expect(refetchedUser?.name, equals('Alice Updated'));
// Delete
await database.users.delete(savedUser.id);
final deletedUser = await database.users.findById(savedUser.id);
expect(deletedUser, isNull);
});
test('transaction rollback', () async {
await database.transaction((txn) async {
await txn.users.insert(User(name: 'Alice', email: '[email protected]'));
await txn.users.insert(User(name: 'Bob', email: '[email protected]'));
throw Exception('Rollback transaction');
}).catchError((_) {});
final users = await database.users.findAll();
expect(users, isEmpty);
});
});
}
Component Integration Tests
// test/integration/user_service_integration_test.dart
import 'package:test/test.dart';
void main() {
group('UserService Integration', () {
late UserService userService;
late UserRepository repository;
late EmailService emailService;
setUp(() {
repository = InMemoryUserRepository();
emailService = MockEmailService();
userService = UserService(
repository: repository,
emailService: emailService,
);
});
test('user registration flow', () async {
final result = await userService.registerUser(
name: 'Alice',
email: '[email protected]',
password: 'password123',
);
expect(result.isSuccess, isTrue);
expect(result.user.name, equals('Alice'));
// Verify user was saved
final savedUser = await repository.findByEmail('[email protected]');
expect(savedUser, isNotNull);
// Verify welcome email was sent
verify(emailService.sendWelcomeEmail(any)).called(1);
});
test('prevents duplicate registration', () async {
await userService.registerUser(
name: 'Alice',
email: '[email protected]',
password: 'password123',
);
final result = await userService.registerUser(
name: 'Alice',
email: '[email protected]',
password: 'password123',
);
expect(result.isSuccess, isFalse);
expect(result.error, contains('already exists'));
});
});
}
Test Fixtures and Test Data
Test Data Management
// test/fixtures/test_data.dart
class TestData {
static User createUser({
String id = '1',
String name = 'Test User',
String email = '[email protected]',
int age = 30,
}) {
return User(
id: id,
name: name,
email: email,
age: age,
);
}
static List<User> createUsers(int count) {
return List.generate(
count,
(i) => createUser(
id: '$i',
name: 'User $i',
email: '[email protected]',
),
);
}
static Product createProduct({
String id = '1',
String name = 'Test Product',
double price = 99.99,
}) {
return Product(
id: id,
name: name,
price: price,
);
}
}
// Usage in tests
void main() {
test('processes user', () {
final user = TestData.createUser(name: 'Alice');
final result = processUser(user);
expect(result, isTrue);
});
test('processes multiple users', () {
final users = TestData.createUsers(10);
expect(users, hasLength(10));
});
}
Loading Test Fixtures from Files
// test/fixtures/users.json
// [
// {"id": "1", "name": "Alice", "email": "[email protected]"},
// {"id": "2", "name": "Bob", "email": "[email protected]"}
// ]
// test/test_helpers.dart
import 'dart:io';
import 'dart:convert';
class FixtureLoader {
static Future<String> loadFixture(String fileName) async {
final file = File('test/fixtures/$fileName');
return await file.readAsString();
}
static Future<List<User>> loadUsers() async {
final json = await loadFixture('users.json');
final list = jsonDecode(json) as List;
return list.map((item) => User.fromJson(item)).toList();
}
static Future<Map<String, dynamic>> loadJsonFixture(String fileName) async {
final json = await loadFixture(fileName);
return jsonDecode(json);
}
}
// Usage
void main() {
test('loads fixture data', () async {
final users = await FixtureLoader.loadUsers();
expect(users, hasLength(2));
expect(users[0].name, equals('Alice'));
});
}
TDD Workflow in Dart
RED-GREEN-REFACTOR Cycle
// Step 1: RED - Write failing test first
// test/services/calculator_service_test.dart
import 'package:test/test.dart';
void main() {
group('CalculatorService', () {
test('calculates total with tax', () {
final calculator = CalculatorService();
final result = calculator.calculateTotalWithTax(100, taxRate: 0.1);
expect(result, equals(110));
});
});
}
// Run: dart test
// Result: FAIL - CalculatorService class doesn't exist
// Step 2: GREEN - Write minimal code to pass
// lib/src/services/calculator_service.dart
class CalculatorService {
double calculateTotalWithTax(double amount, {required double taxRate}) {
return amount + (amount * taxRate);
}
}
// Run: dart test
// Result: PASS
// Step 3: REFACTOR - Improve code while keeping tests green
class CalculatorService {
double calculateTotalWithTax(double amount, {required double taxRate}) {
if (amount < 0) {
throw ArgumentError('Amount must be non-negative');
}
if (taxRate < 0 || taxRate > 1) {
throw ArgumentError('Tax rate must be between 0 and 1');
}
final tax = _calculateTax(amount, taxRate);
return amount + tax;
}
double _calculateTax(double amount, double rate) {
return amount * rate;
}
}
// Add more tests for edge cases
test('throws on negative amount', () {
final calculator = CalculatorService();
expect(
() => calculator.calculateTotalWithTax(-10, taxRate: 0.1),
throwsArgumentError,
);
});
test('throws on invalid tax rate', () {
final calculator = CalculatorService();
expect(
() => calculator.calculateTotalWithTax(100, taxRate: 1.5),
throwsArgumentError,
);
});
// Run tests again: PASS
TDD Best Practices
// 1. Start with the simplest test
test('adds two positive numbers', () {
final calculator = Calculator();
expect(calculator.add(2, 3), equals(5));
});
// 2. Write just enough code to pass
class Calculator {
int add(int a, int b) => 5; // Simplest implementation
}
// 3. Add more specific tests
test('adds different numbers', () {
final calculator = Calculator();
expect(calculator.add(1, 1), equals(2));
expect(calculator.add(10, 5), equals(15));
});
// 4. Now implement properly
class Calculator {
int add(int a, int b) => a + b;
}
// 5. Test edge cases
group('edge cases', () {
late Calculator calculator;
setUp(() {
calculator = Calculator();
});
test('handles zero', () {
expect(calculator.add(0, 0), equals(0));
expect(calculator.add(5, 0), equals(5));
});
test('handles negative numbers', () {
expect(calculator.add(-5, -3), equals(-8));
expect(calculator.add(-5, 10), equals(5));
});
test('handles large numbers', () {
expect(calculator.add(1000000, 2000000), equals(3000000));
});
});
// 6. Refactor with confidence
class Calculator {
int add(int a, int b) {
_validateInput(a);
_validateInput(b);
return _performAddition(a, b);
}
void _validateInput(int value) {
// Add validation if needed
}
int _performAddition(int a, int b) => a + b;
}
Golden Tests (Flutter Compatibility)
Widget Golden Tests
// test/widgets/user_card_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:golden_toolkit/golden_toolkit.dart';
void main() {
group('UserCard Golden Tests', () {
testGoldens('renders correctly', (tester) async {
await loadAppFonts();
final builder = GoldenBuilder.grid(
columns: 2,
widthToHeightRatio: 1,
)
..addScenario(
'Default',
UserCard(
name: 'Alice',
email: '[email protected]',
),
)
..addScenario(
'With Avatar',
UserCard(
name: 'Bob',
email: '[email protected]',
avatarUrl: 'https://example.com/avatar.jpg',
),
)
..addScenario(
'Long Name',
UserCard(
name: 'Christopher Alexander',
email: '[email protected]',
),
);
await tester.pumpWidgetBuilder(builder.build());
await screenMatchesGolden(tester, 'user_card');
});
});
}
// Generate golden files: dart test --update-goldens
// Compare: dart test
Best Practices
DO:
- Write tests FIRST (TDD)
- Use descriptive test names that explain the behavior
- Keep tests simple and focused on one thing
- Use setUp/tearDown for common initialization
- Mock external dependencies
- Test both happy paths and error cases
- Use meaningful matcher messages
- Organize tests with groups
- Clean up resources in tearDown
- Aim for high test coverage (80%+)
DON'T:
- Test private methods directly
- Use sleep() for timing (use FakeAsync)
- Create flaky tests that pass/fail randomly
- Ignore test failures
- Over-mock (prefer real objects when fast)
- Test framework code
- Leave commented-out tests
- Create dependencies between tests
- Test implementation details
Running Tests
# Run all tests
dart test
# Run specific test file
dart test test/services/user_service_test.dart
# Run tests matching name pattern
dart test --name="user"
# Run with coverage
dart test --coverage=coverage
# Run in watch mode
dart test --watch
# Run with verbose output
dart test --verbose
# Run tests in specific directory
dart test test/integration/
# Run with custom concurrency
dart test --concurrency=1
# Run with timeout
dart test --timeout=30s
# Update golden files (Flutter)
flutter test --update-goldens
Summary
Dart testing provides comprehensive tools for writing reliable tests:
- Use dart test package - Core testing framework with groups and matchers
- Organize tests properly - Mirror lib/ structure in test/
- Mock with mockito - Generate type-safe mocks for dependencies
- Test async code - Use async/await, expectLater, emitsInOrder
- Measure coverage - Aim for 80%+ overall coverage
- Integration tests - Test component interactions
- Manage test data - Use fixtures and test data helpers
- Follow TDD - Red-Green-Refactor cycle
- Use matchers effectively - Rich built-in and custom matchers
- Keep tests maintainable - Tests are code too
Remember: Good tests enable confident refactoring and serve as living documentation of expected behavior.