Writing Unit Tests
1. Understanding Test Types
| Type | Description | Use Case | Example |
|---|---|---|---|
| Widget Tests | Test UI components and their interactions. | Verify widget rendering, tap events, and state changes. | |
| Integration Tests | Test app flows across multiple screens or modules. | Validate end-to-end user journeys and data flow. | |
| Unit Tests | Test individual functions or classes in isolation. | Validate logic, calculations, and business rules. | |
2. Writing Unit Tests
Example: Basic Unit Test
import 'package:flutter_test/flutter_test.dart';
void main() {
test('adds one to input values', () {
final calculator = Calculator();
expect(calculator.add(2), equals(3));
});
}
| Feature | Description | Notes |
|---|---|---|
| test() | test(String description, void Function() testFn) |
Defines a single unit test case |
| setUp() | setUp(void Function() setupFn) |
Runs before each test in the group |
| tearDown() | tearDown(void Function() teardownFn) |
Runs after each test in the group |
Note: Unit tests in Flutter use the
flutter_test package and run on the Dart VM.
Example: Testing Widget
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart';
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
await tester.pumpWidget(MaterialApp(home: Text('Hello')));
expect(find.text('Hello'), findsOneWidget);
});
}
| Method | Description | Usage |
|---|---|---|
| testWidgets() | Runs widget tests | Use for UI component testing |
| pumpWidget() | Builds and mounts a widget | Required before finding widgets |
| find.text() | Finds widgets by text content | Use for verifying UI elements |
Test Types in Flutter
- Unit Tests: Test individual functions or classes in isolation
- Widget Tests: Test UI components and their interactions
- Integration Tests: Test full app flows or multiple components together
3. Using test() Function
| Parameter | Type | Description |
|---|---|---|
| name | String |
Name of the test case, used for reporting and debugging. |
| testFn | VoidCallback |
The function containing the test logic to execute. |
| skip | bool |
If true, the test is skipped during execution. |
| timeout | Duration |
Timeout duration for the test execution. |
Example: Basic test() usage
import 'package:flutter_test/flutter_test.dart';
void main() {
test('should return true when adding two positive numbers', () {
final result = 2 + 2;
expect(result, equals(4));
});
}
Example: Test with timeout
test(
'should complete within 5 seconds',
() {
// Simulate a long-running test
expect(true, isTrue);
},
timeout: const Timeout(Duration(seconds: 5)),
);
4. Using expect() Assertions
| Assertion | Description | Example |
|---|---|---|
| expect(value, message) | Checks that the value is truthy or throws an error with the given message | expect(result, isNotNull); |
| expect(value).toEqual(expected) | Checks that the value equals the expected value | expect(person.name, equals('John')); |
| expect(value).toBe(expected) | Checks that the value is identical to the expected value (same object reference) | expect(person, same(expectedPerson)); |
| expect(value).toBeNull() | Checks that the value is null | expect(result, isNull); |
| expect(value).toBeTrue() | Checks that the value is true | expect(isActive, isTrue); |
| expect(value).toBeFalse() | Checks that the value is false | expect(isActive, isFalse); |
| expect(value).toBeLessThan(expected) | Checks that the value is less than the expected value | expect(score, lessThan(100)); |
| expect(value).toBeGreaterThan(expected) | Checks that the value is greater than the expected value | expect(age, greaterThan(18)); |
| expect(value).toBeCloseTo(expected, precision) | Checks that the value is close to the expected value within a given precision | expect(pi, closeTo(3.14, 0.01)); |
| expect(value).toContain(expected) | Checks that the value contains the expected element (for lists or strings) | expect(names, contains('John')); |
| expect(value).toHaveMethod(methodName) | Checks that the value has a method with the given name | expect(person, hasMethod('getName')); |
| expect(value).toHaveProperty(propertyName) | Checks that the value has a property with the given name | expect(person, hasProperty('name')); |
Note: The
expect() function is part of the matcher package in Flutter tests. It provides a fluent API for making assertions about values.Warning: When using
toBe(), be cautious with floating-point numbers, as direct equality checks might fail due to precision issues. Use toBeCloseTo() instead for such cases.5. Using setUp() and tearDown() Hooks
The setUp() and tearDown() hooks are essential for organizing test setup and cleanup in Flutter unit tests. These hooks allow you to define code that runs before and after each test case, ensuring consistent test environments.
| Hook | Description | Usage |
|---|---|---|
| setUp() | Runs before each test case in the test group | Used to initialize variables, mocks, or dependencies needed for tests |
| tearDown() | Runs after each test case in the test group | Used to clean up resources or reset state after each test |
Example: Using setUp() and tearDown()
import 'package:flutter_test/flutter_test.dart';
void main() {
group('User Repository Tests', () {
late UserRepository repo;
late MockDatabase database;
setUp(() {
database = MockDatabase();
repo = UserRepository(database);
});
tearDown(() {
// Clean up any resources if needed
database.clear();
});
test('should fetch user from database', () {
when(database.fetchUser(1)).thenReturn(User(id: 1, name: 'John'));
final user = repo.fetchUser(1);
expect(user.name, 'John');
});
});
}
Note:
setUp() and tearDown() are scoped to the nearest group(). Each test in a group will have its own instance of setup and teardown logic.
6. Using group() for Test Organization
Example: Organizing Tests with group()
import 'package:flutter_test/flutter_test.dart';
void main() {
group('User Authentication', () {
group('Login Flow', () {
test('should login successfully with valid credentials', () async {
// Test implementation
});
test('should show error for invalid password', () async {
// Test implementation
});
});
group('Logout Flow', () {
test('should logout user and clear session', () async {
// Test implementation
});
});
});
}
| Method | Description | Usage |
|---|---|---|
| group() | Groups related tests together for better organization | group('Test Suite Name', () { ... }); |
Note: The
group() function helps in organizing tests into logical blocks, making test suites more readable and maintainable.
7. Testing Async Code
| Method | Description | Example |
|---|---|---|
| testWidgets | Used for testing widget trees that involve async operations. | |
| pumpAndSettle | Waits for all async operations to complete before proceeding. | await tester.pumpAndSettle(Duration(seconds: 5)); |
| pump | Rebuilds the widget tree and waits for a frame to be rendered. | await tester.pump(const Duration(milliseconds: 500)); |
Example: Testing Async API Call
import 'package:flutter_test/flutter_test.dart';
import 'package:http/http.dart' as http;
void main() {
group('Async API Test', () {
test('fetches data from API', () async {
final response = await http.get(Uri.parse('https://api.example.com/data'));
expect(response.statusCode, 200);
});
});
}
Note: When testing async code, always use
await to ensure the test waits for asynchronous operations to complete.
Warning: Avoid using
Future.delayed in tests unless necessary, as it can make tests flaky.
8. Mocking Dependencies
Example: Mocking HTTP Client
import 'package:flutter_test/flutter_test.dart';
import 'package:http/http.dart' as http;
// Mock implementation
class MockHttpClient implements http.Client {
@override
Future<http.Response> get(Uri url) async {
if (url.path == '/users') {
return http.Response('{"name": "John"}', 200);
}
return http.Response('', 404);
}
}
void main() {
test('Fetch user data', () async {
final client = MockHttpClient();
final response = await client.get(Uri.parse('http://example.com/users'));
expect(response.statusCode, 200);
});
}
| Feature | Description | Usage |
|---|---|---|
| mockito | Popular mocking library for Dart | Mockito provides easy-to-use mocking capabilities |
| mocktail | Modern, lightweight mocking library | Alternative to Mockito with simpler API |
Note: When mocking dependencies, ensure that the mock returns consistent data to avoid flaky tests.
Mockito vs Mocktail
- Mockito: More mature, feature-rich, but verbose
- Mocktail: Simpler syntax, less boilerplate
9. Using @GenerateMocks Annotation
The @GenerateMocks annotation in Flutter is used to automatically generate mock classes for your test files. It simplifies the process of creating mocks for dependencies, reducing boilerplate code and making tests more maintainable.
| Annotation | Description | Usage |
|---|---|---|
| @GenerateMocks | Generates mock classes for specified dependencies. | @GenerateMocks([MyService, UserRepository]) |
Example: Using @GenerateMocks
@GenerateMocks([MyService, UserRepository])
void main() {
final mocks = <Mock>[];
// Mocks are generated and can be used in tests
}
Note: The
@GenerateMocks annotation requires the mockito package to be included in your project dependencies.
Warning: Ensure that the
build_runner package is added to your project and run flutter pub run build_runner build after adding the annotation to generate the mock classes.
10. Running Tests
| Flag | Description | Usage |
|---|---|---|
| --coverage | Generates code coverage report | flutter test --coverage |
| --machine | Runs tests in machine-readable format | flutter test --machine |
| --verbose | Shows detailed output during test run | flutter test --verbose |
| --timeout | Sets timeout for test execution | flutter test --timeout 30s |