Testing Fundamentals
Why Test
Section titled “Why Test”Testing is not a phase that comes after development. It is a structural property of the codebase That determines whether you can safely change it. The absence of tests does not mean you are moving Faster — it means every change is a gamble with unknown odds.
Regression Prevention
Section titled “Regression Prevention”A regression is a bug introduced by a change that was supposed to be unrelated. The cost of a Regression grows non-linearly with time: a bug caught by a test running in CI costs seconds to fix; A bug caught by a user in production costs hours of investigation, a release cycle, and lost trust. Tests act as a contract that the system must continue to satisfy after every change.
Documentation
Section titled “Documentation”A test describes the expected behavior of a unit in a form that cannot go out of date — if the test Passes, the behavior matches the description; if it fails, either the behavior changed or the test Needs updating. This is more reliable than comments, which have no mechanism to signal staleness.
Design Improvement
Section titled “Design Improvement”Code that is difficult to test is almost always poorly structured. Tight coupling, hidden Dependencies, side effects in constructors, and god classes all surface as untestable code. Writing Tests first (test-driven development) or writing tests immediately after forces you to confront These design problems while they are still cheap to fix.
Confidence in Refactoring
Section titled “Confidence in Refactoring”Refactoring is the process of changing the internal structure of code without changing its external Behavior. Without tests, you have no way to verify that behavior is preserved. With tests, you can Restructure, rename, split, and merge with confidence — the tests tell you immediately if you broke Something.
Types of Testing
Section titled “Types of Testing”| Type | Scope | Speed | What It Validates |
|---|---|---|---|
| Unit | Single function or class | Millis | Isolated logic correctness |
| Widget | Single Flutter widget | 10-100ms | Rendering, interaction, lifecycle |
| Integration | Multiple units together | 100-1000ms | Unit interactions, data flow across boundaries |
| End-to-End | Full application flow | Seconds | User-visible behavior from launch to completion |
Unit tests are the foundation. They are fast, deterministic, and cheap to maintain. Widget tests add Coverage for the UI layer. Integration tests validate that units compose correctly. End-to-end tests Are the most expensive and should be used sparingly — they catch issues that lower-level tests miss, But they are slow, flaky, and hard to debug.
The testing pyramid is not a suggestion — it is a cost optimization. A project with 1000 unit tests, 100 widget tests, and 10 integration tests will outperform a project with 10 unit tests and 100 Integration tests in both defect detection and developer velocity.
The test Package
Section titled “The test Package”## pubspec.yamldev_dependencies: test: ^1.25.0For Flutter projects, the flutter_test package is the equivalent — it re-exports test and adds Flutter-specific bindings:
## pubspec.yaml (Flutter)dev_dependencies: flutter_test: sdk: flutterTest File Conventions
Section titled “Test File Conventions”Test files live in the test/ directory and mirror the lib/ structure:
lib/ src/ auth/ authenticator.dart token_store.darttest/ src/ auth/ authenticator_test.dart token_store_test.dartEach file under test is suffixed with _test.dart. The test runner discovers tests by convention — No registration is required.
The Core API
Section titled “The Core API”import "package:test/test.dart';
void main() { // Define a single test case test('addition works', () { expect(1 + 1, equals(2)); });
// Group related tests group('Calculator', () { late Calculator calc;
// Runs before each test in this group setUp(() { calc = Calculator(); });
// Runs after each test tearDown(() { calc.dispose(); });
test('adds two numbers', () { expect(calc.add(2, 3), equals(5)); });
test('throws on division by zero', () { expect(() => calc.divide(1, 0), throwsArgumentError); }); });}setUp, tearDown, setUpAll, tearDownAll
Section titled “setUp, tearDown, setUpAll, tearDownAll”setUp and tearDown run before and after each test. setUpAll and tearDownAll run once Before and after all tests in the group. The distinction matters for expensive resources:
group('Database Integration', () { static late Database db;
setUpAll(() async { db = await Database.connect('postgres://localhost/test_db'); await db.migrate(); });
tearDownAll(() async { await db.close(); });
setUp(() async { await db.truncateAllTables(); });
test('inserts a user', () async { final id = await db.insertUser(name: "Alice''); expect(id, isNotNull); });
test("fetches user by id', () async { final id = await db.insertUser(name: "Bob''); final user = await db.fetchUser(id); expect(user.name, equals("Bob')); });});setUpAll is for resources that are expensive to create (database connections, HTTP servers, test Fixtures). setUp is for resetting state between tests. Never share mutable state across tests Without resetting it — tests must be independent and order-invariant.
expect and the Matcher System
Section titled “expect and the Matcher System”expect(actual, matcher) is the core assertion. The actual value is evaluated immediately; the matcher describes the expected property. When the match fails, the test framework produces a Human-readable diff:
expect(actual, equals(expected));// Fails with:// Expected: <expected value>// Actual: <actual value>The matcher system is composable — you can negate, combine, and nest matchers:
expect(list, isNot(isEmpty));expect(value, allOf(isA<int>(), greaterThan(0), lessThan(100)));Matchers
Section titled “Matchers”Equality and Identity
Section titled “Equality and Identity”expect(42, equals(42));expect(a, isNot(equals(b)));
// Identity — checks if two references point to the same objectfinal obj = Object();expect(obj, same(obj)); // Passesexpect(obj, same(Object())); // Failsequals uses the == operator. same uses identical(). Use equals for value equality and same when you need reference identity (e.g., verifying that a factory returns a cached instance).
Boolean
Section titled “Boolean”expect(true, isTrue);expect(false, isFalse);expect(null, isNull);expect(value, isNotNull);Type Checking
Section titled “Type Checking”expect(value, isA<String>());expect(value, isA<List<int>>());
// Combined with property access after type checkexpect(value, isA<User>().having((u) => u.name, 'name', 'Alice'));isA<T>() checks value is T at runtime. The .having() combinator lets you assert properties of The matched object — the second argument is a description used in failure messages.
Numeric Comparisons
Section titled “Numeric Comparisons”expect(10, greaterThan(5));expect(3, lessThan(5));expect(5, greaterThanOrEqualTo(5));expect(5, lessThanOrEqualTo(5));expect(3.14, closeTo(3.0, 0.2)); // |actual - expected| <= deltacloseTo is essential for floating-point comparisons. Never use equals for doubles — rounding Errors will cause flaky tests.
Collections
Section titled “Collections”// Containsexpect([1, 2, 3], contains(2));expect({'a': 1, 'b': 2}, containsPair('a', 1));
// Emptyexpect([], isEmpty);expect([1], isNotEmpty);
// Every element matchesexpect([2, 4, 6], everyElement(isEven));
// Ordered matchingexpect([1, 2, 3], equals([1, 2, 3]));expect([1, 2, 3], containsAll([2, 1])); // subset, order-independent
// Unordered equalityexpect([3, 1, 2], unorderedEquals([1, 2, 3]));
// Pairwise comparisonexpect([[1, 2], [3, 4]], pairwise(equals, [[1, 2], [3, 4]]));Exception Matchers
Section titled “Exception Matchers”// Generic throw checkexpect(() => throw Exception('boom'), throwsA(anything));
// Specific typeexpect(() => throw ArgumentError('bad'), throwsArgumentError);expect(() => throw StateError('bad'), throwsStateError);expect(() => throw Exception('bad'), throwsException);
// With message matchingexpect( () => throw FormatException('invalid input'), throwsA(isA<FormatException>().having((e) => e.message, 'message', 'invalid input')),);String Matchers
Section titled “String Matchers”expect('hello world', contains('world'));expect('hello world', startsWith('hello'));expect('hello world', endsWith('world'));expect('hello', matches(r'^[a-z]+$')); // regexexpect('hello', equalsIgnoringCase('HELLO'));Custom Matchers
Section titled “Custom Matchers”When the built-in matchers do not express the property you need, write a custom matcher:
import 'package:test/test.dart';
class IsValidEmail extends Matcher { static final _emailRegex = RegExp( r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$', );
@override bool matches(Object? item, Map matchState) { return item is String && _emailRegex.hasMatch(item); }
@override Description describe(Description description) { return description.add('a valid email address'); }
@override Description describeMismatch( Object? item, Description mismatchDescription, Map matchState, bool verbose, ) { return mismatchDescription.add('was not a valid email: ").addDescriptionOf(item); }}
// UsageMatcher get isValidEmail => IsValidEmail();
test(''email validation", () { expect('user@example.com', isValidEmail); expect('not-an-email', isNot(isValidEmail));});For more complex matchers, extend CustomMatcher which provides featureValueOf for property Extraction:
class HasLength extends CustomMatcher { HasLength(Matcher lengthMatcher) : super('has length', 'length', lengthMatcher);
@override Object? featureValueOf(Object? actual) { if (actual is List) return actual.length; if (actual is String) return actual.length; if (actual is Map) return actual.length; return null; }}
// Usageexpect([1, 2, 3], HasLength(equals(3)));expect('hello', HasLength(greaterThan(0)));Unit Testing
Section titled “Unit Testing”Testing Pure Functions
Section titled “Testing Pure Functions”Pure functions are the easiest to test — no dependencies, no side effects, no setup. Input goes in, Output comes out:
double circleArea(double radius) { if (radius < 0) throw ArgumentError('Radius must be non-negative'); return 3.141592653589793 * radius * radius;}
// test/src/math/geometry_test.dartimport 'package:test/test.dart';import 'package:myapp/src/math/geometry.dart';
void main() { group('circleArea', () { test('returns correct area for positive radius', () { expect(circleArea(1), closeTo(3.14159, 0.00001)); expect(circleArea(2), closeTo(12.56637, 0.00001)); expect(circleArea(0), equals(0.0)); });
test('throws ArgumentError for negative radius', () { expect(() => circleArea(-1), throwsArgumentError); }); });}Testing Classes with Dependencies
Section titled “Testing Classes with Dependencies”When a class depends on external services (repositories, API clients, caches), inject those Dependencies as abstract interfaces and substitute test doubles:
abstract class TokenStore { Future<String?> get(); Future<void> save(String token); Future<void> clear();}
abstract class AuthApi { Future<String> login(String email, String password);}
class Authenticator { final TokenStore _tokenStore; final AuthApi _api;
Authenticator(this._tokenStore, this._api);
Future<bool> authenticate(String email, String password) async { final cached = await _tokenStore.get(); if (cached != null) return true;
final token = await _api.login(email, password); await _tokenStore.save(token); return true; }
Future<void> logout() async { await _tokenStore.clear(); }}import 'package:test/test.dart';import 'package:mockito/mockito.dart';import 'package:myapp/src/auth/authenticator.dart';
class FakeTokenStore implements TokenStore { String? _token; @override Future<String?> get() async => _token; @override Future<void> save(String token) async => _token = token; @override Future<void> clear() async => _token = null;}
class MockAuthApi extends Mock implements AuthApi {}
void main() { group('Authenticator', () { late Authenticator auth; late FakeTokenStore tokenStore; late MockAuthApi api;
setUp(() { tokenStore = FakeTokenStore(); api = MockAuthApi(); auth = Authenticator(tokenStore, api); });
test('returns true when cached token exists', () async { await tokenStore.save('cached-token'); final result = await auth.authenticate('a@b.com', 'pass'); expect(result, isTrue); verifyNever(api.login(any, any)); });
test('calls API when no cached token, then saves', () async { when(api.login('a@b.com', 'pass')) .thenAnswer((_) async => 'new-token');
final result = await auth.authenticate('a@b.com', 'pass');
expect(result, isTrue); verify(api.login('a@b.com', 'pass')).called(1); expect(await tokenStore.get(), equals('new-token')); });
test('logout clears token store', () async { await tokenStore.save('token'); await auth.logout(); expect(await tokenStore.get(), isNull); }); });}Arrange-Act-Assert Pattern
Section titled “Arrange-Act-Assert Pattern”Every test follows three phases:
- Arrange. Set up the system under test and its inputs.
- Act. Invoke the behavior being tested.
- Assert. Verify the output or side effects.
test('calculates total with tax', () { // Arrange final cart = Cart(taxRate: 0.1); cart.addItem(Item(price: 10.0)); cart.addItem(Item(price: 20.0));
// Act final total = cart.total;
// Assert expect(total, closeTo(33.0, 0.001));});Keep the phases visually distinct with blank lines. This makes tests scannable and makes it obvious When a test is doing too much in one phase.
Test Organization
Section titled “Test Organization”Mirror the source structure. If lib/src/orders/order_service.dart contains OrderServiceThe Test lives at test/src/orders/order_service_test.dart. This is not just convention — it makes Navigation trivial when the directory tree is the same in both lib/ and test/.
Within a test file, use group() to organize by method, then by scenario:
void main() { group('OrderService', () { late OrderService service; late MockOrderRepo repo; late MockPaymentGateway payment;
setUp(() { /* ... */ });
group('createOrder', () { test('creates order when inventory is available', () { /* ... */ }); test('throws when item is out of stock', () { /* ... */ }); test('charges payment after order creation', () { /* ... */ }); });
group('cancelOrder', () { test('refunds payment when order is cancellable', () { /* ... */ }); test('throws when order has already shipped', () { /* ... */ }); }); });}Mocking
Section titled “Mocking”mockito
Section titled “mockito”mockito is the dominant mocking library in the Dart ecosystem. It uses code generation to create Mock classes that record calls and return configurable values.
dev_dependencies: mockito: ^5.4.0 build_runner: ^2.4.0Generate mocks with a file annotation:
import 'package:mockito/annotations.dart';import 'package:myapp/src/repositories/user_repository.dart';import 'package:myapp/src/services/analytics_service.dart';
@GenerateMocks([UserRepository, AnalyticsService])void main() {}Run the generator:
dart run build_runner buildThis produces mocks.mocks.dart with MockUserRepository and MockAnalyticsService classes.
Stubbing with when/thenReturn
Section titled “Stubbing with when/thenReturn”import 'package:mockito/mockito.dart';import 'package:mockito/annotations.dart';import 'mocks.mocks.dart';
void main() { late MockUserRepository repo;
setUp(() { repo = MockUserRepository(); });
test('fetches user by id', () async { // Stub: when this method is called with this argument, return this value when(repo.findById('123')) .thenAnswer((_) async => User(id: "123'', name: "Alice'));
final user = await repo.findById('123'); expect(user.name, equals('Alice')); });
test('returns null for non-existent user', () async { when(repo.findById('999')) .thenAnswer((_) async => null);
final user = await repo.findById('999'); expect(user, isNull); });});thenAnswer receives the invocation as an argument, which allows dynamic responses:
when(repo.findById(any)) .thenAnswer((invocation) async { final id = invocation.positionalArguments[0] as String; return User(id: id, name: "User-$id''); });For simple cases, thenReturn is shorthand:
when(repo.count()).thenReturn(42);when(repo.findById(any)).thenReturn(null);Verification with verify
Section titled “Verification with verify”test("logs analytics event on login', () async { final analytics = MockAnalyticsService(); final service = AuthService(repo, analytics);
await service.login('alice@example.com', 'password');
// Verify the method was called exactly once with these arguments verify(analytics.trackEvent('login', {'email': "alice@example.com''})) .called(1);
// Verify it was called at least once (any arguments) verify(analytics.trackEvent(any, any)).called(greaterThan(0));
// Verify it was never called verifyNever(analytics.trackEvent("logout', any));});reset(mock) clears all stubbings and call history on a mock. Use it when you want to reuse a mock Across tests without recreating it:
tearDown(() { reset(repo);});Argument Matchers
Section titled “Argument Matchers”Mockito provides argument matchers for flexible stubbing and verification:
import 'package:mockito/annotations.dart';
// Match any valuewhen(repo.findById(any)).thenReturn(null);
// Match any named parameter valuewhen(repo.search(name: anyNamed('name'))).thenReturn([]);
// Match any value of a specific typewhen(repo.save(anyThat(isA<AdminUser>()))) .thenAnswer((_) async => true);
// Capture arguments for inspectiontest('sends correct notification', () async { final notificationService = MockNotificationService(); await service.processOrder(order);
final captured = verify( notificationService.send(captureAny, captureAny), ).captured; expect(captured[0], equals('user-123')); expect(captured[1], isA<OrderConfirmation>());});mocktail
Section titled “mocktail”mocktail is a simpler alternative that does not require code generation. You extend Mock Directly:
dev_dependencies: mocktail: ^1.0.0import 'package:mocktail/mocktail.dart';
class MockUserRepository extends Mock implements UserRepository {}
void main() { late MockUserRepository repo;
setUp(() { repo = MockUserRepository(); // mocktail requires you to register fallback values for non-nullable parameters registerFallbackValue(User(id: "0'', name: "fallback')); });
test('fetches user', () async { when(() => repo.findById('123')) .thenAnswer((_) async => User(id: "123'', name: "Alice'));
final user = await repo.findById('123'); expect(user.name, equals('Alice')); });}mockito vs mocktail
Section titled “mockito vs mocktail”| Factor | mockito | mocktail |
|---|---|---|
| Code gen | Required (build_runner) | None |
| Setup | Annotate classes, run generator | Extend Mock directly |
| Fallback vals | Not required | Required for non-nullable params |
| API | when(mock.method()).thenReturn(...) | when(() => mock.method())... |
| Ecosystem | Larger, more mature | Lighter, simpler |
| Type safety | Strong (generated stubs) | Weaker (manual implements) |
Use mocktail for small projects or when you want zero build-time overhead. Use mockito for Larger projects where generated mocks provide stronger type guarantees and better IDE support.
Async Testing
Section titled “Async Testing”Testing Future-Returning Functions
Section titled “Testing Future-Returning Functions”A test body can be async. The test runner awaitS the returned Future:
test('fetches data from API', () async { final service = DataService(client: mockClient); final result = await service.fetchUsers();
expect(result, hasLength(3)); expect(result[0].name, equals('Alice'));});If the Future completes with an error and no try/catch wraps it, the test fails with the error Message and stack trace. This is correct behavior — the test framework catches the error for you:
test('propagates API errors', () async { when(mockClient.get(any)).thenThrow(SocketException('network down'));
final service = DataService(client: mockClient);
// This test passes because it expects the Future to throw await expectLater( service.fetchUsers(), throwsA(isA<SocketException>()), );});expectLater for Streams
Section titled “expectLater for Streams”expectLater returns a Future that completes when the matcher is satisfied. This is essential for Testing streams because you cannot await a stream in a normal expect:
test('emits values in order', () { final controller = StreamController<int>(); final stream = controller.stream;
expectLater( stream, emitsInOrder([1, 2, 3, emitsDone]), );
controller.add(1); controller.add(2); controller.add(3); controller.close();});Stream Matchers
Section titled “Stream Matchers”import 'package:test/test.dart';
// Emits exactly these values in this order, then doneexpectLater(stream, emitsInOrder([1, 2, 3, emitsDone]));
// Emits these values (ignoring order) then doneexpectLater(stream, emitsUnorderedEvents([3, 1, 2, emitsDone]));
// Emits a single value matching a matcherexpectLater(stream, emits(equals(42)));
// Emits an error matching a matcherexpectLater(stream, emitsError(isA<StateError>()));
// Emits any errorexpectLater(stream, emitsError(anything));
// Never emits (stream completes empty)expectLater(stream, neverEmits(anything));
// Combining — emits 1, then 2, then any error, then doneexpectLater( stream, emitsInOrder([1, 2, emitsError(anything), emitsDone]),);FakeAsync and the async Package
Section titled “FakeAsync and the async Package”FakeAsync from package:async gives you deterministic control over time in tests. It replaces the Real event loop with a fake one that only advances when you tell it to:
dev_dependencies: async: ^2.11.0import 'package:async/async.dart';import 'package:test/test.dart';
test('debounce fires after 500ms of inactivity', () { FakeAsync().run((fakeAsync) { final callLog = <int>[]; final debounced = debounce<int>( (value) => callLog.add(value), const Duration(milliseconds: 500), );
debounced(1); debounced(2); debounced(3);
// No calls yet — debounce period has not elapsed expect(callLog, isEmpty);
// Advance time by 499ms — still within debounce window fakeAsync.elapse(const Duration(milliseconds: 499)); expect(callLog, isEmpty);
// Advance past the debounce threshold fakeAsync.elapse(const Duration(milliseconds: 1)); expect(callLog, equals([3])); // Only the last value fires });});
test('timeout fires after deadline', () { FakeAsync().run((fakeAsync) { var completed = false; var timedOut = false;
Future.delayed(const Duration(seconds: 5)).then((_) { completed = true; }).timeout( const Duration(seconds: 2), onTimeout: () { timedOut = true; return null; }, );
fakeAsync.elapse(const Duration(seconds: 1)); expect(completed, isFalse); expect(timedOut, isFalse);
fakeAsync.elapse(const Duration(seconds: 1)); expect(timedOut, isTrue); expect(completed, isFalse);
fakeAsync.elapse(const Duration(seconds: 3)); expect(completed, isTrue); });});FakeAsync is indispensable for testing timers, debounces, timeouts, and any time-dependent logic. Without it, tests that depend on real time are slow and non-deterministic.
Testing Completer-Based APIs
Section titled “Testing Completer-Based APIs”test('resolves when completer completes', () async { final completer = Completer<String>(); final future = completer.future;
completer.complete('done'); expect(await future, equals('done'));});
test('rejects when completer completes with error', () async { final completer = Completer<String>(); final future = completer.future;
completer.completeError(StateError('failed')); await expectLater(future, throwsStateError);});Property-Based Testing
Section titled “Property-Based Testing”Traditional tests check specific inputs against expected outputs. Property-based testing checks that A property (invariant) holds across a large space of random inputs. This catches edge cases that Hand-written tests miss.
Using check (package:check_v1)
Section titled “Using check (package:check_v1)”dev_dependencies: check_v1: ^0.2.0import 'package:check_v1/check_v1.dart';
void main() { test('sort is idempotent', () { check(listOf(numericInt)).satisfies((list) { final sorted = List<int>.from(list)..sort(); final sortedAgain = List<int>.from(sorted)..sort(); return sortedAgain.equals(sorted); }).withLabel('sorting twice produces the same result as sorting once'); });
test('sort preserves length', () { check(listOf(numericInt)).satisfies((list) { final sorted = List<int>.from(list)..sort(); return sorted.length == list.length; }).withLabel('sorted list has same length as input'); });
test('sort produces ordered output', () { check(listOf(numericInt)).satisfies((list) { final sorted = List<int>.from(list)..sort(); for (var i = 1; i < sorted.length; i++) { if (sorted[i] < sorted[i - 1]) return false; } return true; }).withLabel('sorted list is in non-decreasing order'); });
test('reverse is its own inverse', () { check(listOf(numericInt)).satisfies((list) { final reversed = list.reversed.toList(); final doubleReversed = reversed.reversed.toList(); return listEquals(list, doubleReversed); }).withLabel('reversing twice returns the original list'); });}Generators
Section titled “Generators”check provides generators for common types:
// Built-in generatorscheck(numericInt) // Random intcheck(numericDouble) // Random doublecheck(alphaString) // Random stringcheck(listOf(numericInt)) // Random list of intscheck(mapOf(numericInt, alphaString)) // Random map
// Custom generatorfinal positiveInts = numericInt.where((n) => n > 0);final nonEmptyStrings = alphaString.where((s) => s.isNotEmpty);final emailLike = alphaString.map((s) => '$s@example.com');When to Use Property-Based Testing
Section titled “When to Use Property-Based Testing”Use it for:
- Pure functions with well-defined invariants (sorting, serialization, parsing).
- Data transformations where the relationship between input and output can be expressed as a rule.
- Round-trip properties:
parse(serialize(x)) == x.
Do not use it for:
- Functions with side effects (I/O, database writes).
- Functions that depend on external state.
- Properties that are expensive to check (O(n^2) or worse for large inputs).
Property-based tests are complementary to example-based tests. They find edge cases; example-based Tests document specific behaviors. Use both.
Test Coverage
Section titled “Test Coverage”Generating Coverage Reports
Section titled “Generating Coverage Reports”# Dartdart test --coverage=coverage
# Flutterflutter test --coverageThis produces coverage/lcov.info — a machine-readable file listing every line in your source code And whether it was executed during the test run.
Viewing Coverage with genhtml
Section titled “Viewing Coverage with genhtml”# Install lcov (if not already installed)# macOS: brew install lcov# Linux: apt install lcov
# Generate HTML reportgenhtml coverage/lcov.info -o coverage/html
# Open in browseropen coverage/html/index.htmlEach source file gets a page showing which lines are covered (green), uncovered (red), or not Relevant (gray).
Coverage Thresholds in CI
Section titled “Coverage Thresholds in CI”Enforce minimum coverage in CI to prevent regression:
# Fail if overall coverage is below 80%flutter test --coveragelcov --summary coverage/lcov.info# Parse the summary output and fail if below thresholdIn GitHub Actions:
- name: Run tests with coverage run: flutter test --coverage
- name: Check coverage threshold run: | COVERAGE=$(lcov --summary coverage/lcov.info 2>&1 | grep -oP '\d+\.\d+%') echo "Coverage: $COVERAGE" # Fail if below 80% if (( $(echo "$COVERAGE < 80.0" | bc -l) )); then echo "::error::Coverage $COVERAGE is below 80% threshold" exit 1 fiInterpreting Coverage Reports
Section titled “Interpreting Coverage Reports”Coverage is a necessary but not sufficient metric. 100% line coverage does not mean 100% Correctness. Coverage tells you what code was executed, not whether the execution was verified.
// This function has 100% line coverage but zero assertion coverageint divide(int a, int b) { if (b == 0) return 0; // Line covered by one test return a ~/ b; // Line covered by another test}
// But no test verifies that divide(6, 2) == 3// The test calls divide(6, 0) and divide(6, 2) but only checks:test('divide does not crash', () { expect(divide(6, 0), isA<int>()); expect(divide(6, 2), isA<int>());});Branch coverage is more informative than line coverage — it measures whether each conditional branch Was taken. Some tools support this, but the Dart ecosystem’s support is limited. Focus on line Coverage as a floor and supplement with manual review of assertion quality.
Excluding Files from Coverage
Section titled “Excluding Files from Coverage”Create a .covfilter or pass exclusions:
genhtml coverage/lcov.info \ --ignore-errors source \ --rc genhtml_hi_limit=100 \ --rc genhtml_med_limit=80 \ -o coverage/htmlExclude generated files, test helpers, and platform-specific code that cannot be tested on the CI Platform.
Running Tests
Section titled “Running Tests”dart test vs flutter test
Section titled “dart test vs flutter test”dart test— runs pure Dart tests (no Flutter SDK). Use for packages, servers, CLI tools.flutter test— runs Flutter-aware tests (includes widget testing, golden tests). Use for Flutter apps and packages that depend on Flutter.
Filtering Tests
Section titled “Filtering Tests”# Run tests matching a name patternflutter test --name "calculator"
# Run tests NOT matching a patternflutter test --name "slow" --exclude-tags slow
# Run tests with a specific tag# In test file:// @Tags(['integration'])// test('full flow', () { ... });
# Run only unit tests (not tagged as integration)flutter test --exclude-tags integration
# Run only integration testsflutter test --tags integrationTags are defined in the test file with a comment before the test:
@Tags(['slow'])test('processes 10k records', () async { // This test takes several seconds}, timeout: Timeout(Duration(minutes: 1)));Parallelism
Section titled “Parallelism”The test runner runs tests concurrently by default, using multiple isolates:
# Default: runs on all available coresflutter test
# Limit to 2 concurrent isolates (useful for resource-constrained CI)flutter test -j 2
# Run all tests sequentially (useful for debugging test isolation issues)flutter test -j 1Tests must be independent — they must not share mutable state, depend on execution order, or compete For external resources (ports, files). If tests fail under parallelism but pass sequentially, you Have a test isolation bug.
Reporters
Section titled “Reporters”# Default: compact (one line per test)flutter test --reporter compact
# Expanded: one line per assertion failure with full contextflutter test --reporter expanded
# JSON: machine-readable output for CI integrationflutter test --reporter json
# GitHub: prints GitHub Actions annotations for failuresflutter test --reporter githubPlatform Selection
Section titled “Platform Selection”@TestOn('browser')import 'package:test/test.dart';
void main() { // This test only runs in browser environments test('localStorage works', () { // ... });}# Run only browser testsdart test -p chrome
# Run only VM testsdart test -p vm
# Run tests on multiple platformsdart test -p vm,chromeTimeouts
Section titled “Timeouts”Each test has a default timeout of 30 seconds. Override per-test:
test('slow operation', () async { await longRunningOperation();}, timeout: Timeout(Duration(minutes: 2)));Or globally in dart_test.yaml:
timeout: 60sIntuition
Section titled “Intuition”Quality assurance: Testing is like proof-reading your code — it catches errors before they reach users. Different test types verify different aspects of your code.
Why it matters: Good testing practices prevent bugs and improve code quality. Automated tests give you confidence to refactor and add features.
The key insight: Test behavior, not implementation — focus on what your code does, not how it does it.
Common Pitfalls
Section titled “Common Pitfalls”1. Testing Implementation Instead of Behavior
Section titled “1. Testing Implementation Instead of Behavior”// WRONG — couples test to internal implementationtest('adds item to _items list', () { cart.addItem(item); expect(cart._items, contains(item)); // Accessing private field});
// CORRECT — tests the observable behaviortest('total increases when item is added', () { cart.addItem(Item(price: 10.0)); expect(cart.total, equals(10.0));});Tests that reach into private internals break when you refactor. Tests that verify public behavior Survive refactoring.
2. Tests That Depend on Execution Order
Section titled “2. Tests That Depend on Execution Order”// WRONG — test B depends on test A having runtest('A: creates user', () async { await service.createUser('alice');});
test('B: fetches user created by A', () async { final user = await service.fetchUser('alice'); expect(user, isNotNull); // Fails if run alone or in different order});
// CORRECT — each test sets up its own statetest('fetches existing user', () async { await service.createUser('alice'); final user = await service.fetchUser('alice'); expect(user, isNotNull);});3. Over-Mocking
Section titled “3. Over-Mocking”// WRONG — the mock is so complex it duplicates the real implementationwhen(repo.findById(any)).thenAnswer((invocation) { final id = invocation.positionalArguments[0] as String; if (id == '123') return Future.value(User(id: "123'', name: "Alice')); if (id == '456') return Future.value(User(id: "456'', name: "Bob')); return Future.value(null);});
// CORRECT — use a simple in-memory fake or limit the stub to what the test needswhen(repo.findById('123')) .thenAnswer((_) async => User(id: "123'', name: "Alice'));If your mock setup is longer than the test body, you are testing the mock, not the system.
4. Ignoring Async Errors
Section titled “4. Ignoring Async Errors”// WRONG — the async error in the Future is silently losttest('handles error', () async { service.doWork(); // Returns Future — not awaited, no error handling});
// CORRECT — await or use expectLatertest('handles error', () async { await expectLater( service.doWork(), throwsA(isA<NetworkException>()), );});5. Floating-Point Equality
Section titled “5. Floating-Point Equality”// WRONG — floating-point rounding will cause intermittent failuresexpect(0.1 + 0.2, equals(0.3)); // Fails: 0.30000000000000004 != 0.3
// CORRECT — use closeTo with an appropriate toleranceexpect(0.1 + 0.2, closeTo(0.3, 1e-10));6. Tests That Pass Vacuously
Section titled “6. Tests That Pass Vacuously”// WRONG — the assertion always passestest('user is valid', () { final user = User(name: "Alice'', age: 30); expect(user, isNotNull); // Trivially true for any non-null object});
// CORRECT — assert meaningful propertiestest("user has correct name and age', () { final user = User(name: "Alice'', age: 30); expect(user.name, equals("Alice')); expect(user.age, equals(30));});7. Shared Mutable State Across Tests
Section titled “7. Shared Mutable State Across Tests”// WRONG — static state leaks between testsclass Config { static String environment = 'test';}
test('A: sets environment', () { Config.environment = 'production'; expect(Config.environment, equals('production'));});
test('B: expects default environment', () { expect(Config.environment, equals('test')); // Fails if A runs first});
// CORRECT — reset in setUp or use instance-based state8. Not Using FakeAsync for Timer-Dependent Tests
Section titled “8. Not Using FakeAsync for Timer-Dependent Tests”// WRONG — real timeout makes test slow and flakytest('debounce works', () async { final callLog = <int>[]; final debounced = debounce(callLog.add, Duration(seconds: 1)); debounced(1); debounced(2); await Future.delayed(Duration(seconds: 2)); expect(callLog, equals([2]));}, timeout: Timeout(Duration(seconds: 5)));
// CORRECT — deterministic with FakeAsynctest('debounce works', () { FakeAsync().run((fake) { final callLog = <int>[]; final debounced = debounce(callLog.add, Duration(seconds: 1)); debounced(1); debounced(2); fake.elapse(Duration(milliseconds: 999)); expect(callLog, isEmpty); fake.elapse(Duration(milliseconds: 1)); expect(callLog, equals([2])); });});9. Golden Tests Without Baseline Updates
Section titled “9. Golden Tests Without Baseline Updates”Golden tests compare widget rendering against a saved image. When you intentionally change the UI, The golden test fails until you update the baseline. Forgetting to update leads to red tests that Everyone ignores — at which point the golden test has zero value. Run flutter test --update-goldens when you intentionally change the UI, and review the diff carefully.
10. Coverage as a Vanity Metric
Section titled “10. Coverage as a Vanity Metric”100% coverage is not the goal. A test suite with 80% coverage and strong assertions is more valuable Than one with 100% coverage and weak assertions. Coverage measures execution, not correctness. Use It as a floor, not a ceiling, and supplement it with code review of assertion quality.
Summary
Section titled “Summary”This topic covers the core concepts of testing fundamentals, including underlying theory, practical implementation, and key applications.
Key concepts include:
- CPU architecture and the fetch-decode-execute cycle
- memory hierarchy (cache, RAM, virtual)
- input/output systems
- operating systems and scheduling
- interrupts and polling
Understanding these concepts thoroughly is essential for both examinations and practical programming, and requires both theoretical knowledge and hands-on practice.
Worked Examples
Section titled “Worked Examples”Worked examples demonstrating the application of key concepts are covered in the detailed sub-pages linked above.
Cross-References
Section titled “Cross-References”- Dart Null Safety — Null safety catches many potential bugs at compile time, complementing runtime testing.
- Dart Asynchronous Programming — Testing async code requires understanding of futures, streams, and isolate patterns.
- Dart Classes and Objects — Dependency injection and mocking in tests rely on OOP principles and interfaces.
- Rust Error Handling — Testing error conditions and edge cases is a shared concern across Dart and Rust development.