flutter-tester
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseFlutter Tester
Flutter 测试实践指南
Requirements
前提条件
- Flutter project with dependency
flutter_test - Works with Riverpod, Mockito, and GetIt
- Run to generate mocks after adding
dart run build_runner buildannotations@GenerateMocks - Compatible with FVM (instead of
fvm flutter test)flutter test
- 包含依赖的Flutter项目
flutter_test - 兼容Riverpod、Mockito和GetIt
- 添加注解后,运行
@GenerateMocks生成模拟对象dart run build_runner build - 支持FVM(使用替代
fvm flutter test)flutter test
Overview
概述
Test each architectural layer in isolation using Given-When-Then structure. Always test both success and error paths. Never mock providers — override their dependencies instead.
采用Given-When-Then结构,独立测试每个架构分层。务必同时测试成功路径和错误路径。切勿直接模拟Provider,而是覆盖其依赖项。
Reference Files
参考文件
Load the relevant file based on what you're testing:
| What you're testing | Reference file |
|---|---|
| Repository, DAO, Service logic | |
| Widget UI, interactions, dialogs, navigation | |
| Riverpod provider state, mutations, lifecycle | |
根据测试内容加载对应的参考文件:
| 测试内容 | 参考文件 |
|---|---|
| Repository、DAO、Service逻辑 | |
| Widget界面、交互、弹窗、导航 | |
| Riverpod Provider状态、变更、生命周期 | |
Core Principles
核心原则
1. Layer Isolation
1. 分层隔离
Test each layer against its own mocked dependencies:
| Layer | What to test | What to mock |
|---|---|---|
| Repository | Data coordination between sources | DAOs, APIs, Logger |
| DAO | Database CRUD operations | Use real in-memory DB, mock Logger |
| Provider | State management and transitions | Services, Repositories |
| Service | Business logic and workflows | Repositories, Network clients |
| Widget | UI behaviour and interactions | Provider dependencies (via overrides) |
针对每个分层,使用模拟的依赖项进行测试:
| 分层 | 测试内容 | 模拟对象 |
|---|---|---|
| Repository | 多数据源间的数据协调逻辑 | DAO、API、Logger |
| DAO | 数据库CRUD操作 | 使用真实内存数据库,模拟Logger |
| Provider | 状态管理与状态转换 | Service、Repository |
| Service | 业务逻辑与工作流 | Repository、网络客户端 |
| Widget | UI行为与交互 | Provider依赖项(通过覆盖方式) |
2. Given-When-Then Structure
2. Given-When-Then结构
dart
test('Given valid data, When fetchUsers called, Then returns user list', () async {
// Arrange (Given)
when(mockDAO.fetchAll()).thenAnswer((_) async => expectedUsers);
// Act (When)
final result = await repository.fetchUsers();
// Assert (Then)
expect(result, equals(expectedUsers));
verify(mockDAO.fetchAll()).called(1);
});dart
test('Given valid data, When fetchUsers called, Then returns user list', () async {
// Arrange (Given)
when(mockDAO.fetchAll()).thenAnswer((_) async => expectedUsers);
// Act (When)
final result = await repository.fetchUsers();
// Assert (Then)
expect(result, equals(expectedUsers));
verify(mockDAO.fetchAll()).called(1);
});3. Test Organisation
3. 测试组织
dart
group('UserRepository', () {
group('fetchUsers', () {
setUp(() { /* init mocks, register with GetIt */ });
tearDown(() => GetIt.I.reset()); // Always reset GetIt
test('Given success ... When ... Then ...', () { });
test('Given error ... When ... Then ...', () { });
});
});dart
group('UserRepository', () {
group('fetchUsers', () {
setUp(() { /* 初始化模拟对象,注册到GetIt */ });
tearDown(() => GetIt.I.reset()); // 务必重置GetIt
test('Given success ... When ... Then ...', () { });
test('Given error ... When ... Then ...', () { });
});
});Standard Test Setup
标准测试配置
Generate Mocks
生成模拟对象
dart
([IUserDAO, IUserAPI, ILogger])
void main() { ... }Run after modifying .
dart run build_runner build@GenerateMocksdart
([IUserDAO, IUserAPI, ILogger])
void main() { ... }修改后,运行。
@GenerateMocksdart run build_runner buildRegister with GetIt
注册到GetIt
dart
setUp(() {
mockDAO = MockIUserDAO();
mockLogger = MockILogger();
GetIt.I
..registerSingleton<IUserDAO>(mockDAO)
..registerSingleton<ILogger>(mockLogger);
});
tearDown(() => GetIt.I.reset()); // Critical — always resetdart
setUp(() {
mockDAO = MockIUserDAO();
mockLogger = MockILogger();
GetIt.I
..registerSingleton<IUserDAO>(mockDAO)
..registerSingleton<ILogger>(mockLogger);
});
tearDown(() => GetIt.I.reset()); // 关键步骤 — 务必重置Fakes vs Mocks
假实现(Fakes)与模拟对象(Mocks)
- Fakes () — silent stubs; use when you don't need to verify calls
class FakeLogger extends ILogger - Mocks () — use when you need
MockILogger,when(), orverify()thenThrow()
- Fakes ()—— 静默桩;无需验证调用时使用
class FakeLogger extends ILogger - Mocks ()—— 需要使用
MockILogger、when()或verify()时使用thenThrow()
Quick Reference
快速参考
| Scenario | Key pattern |
|---|---|
| Test a repository | Mock DAO + API → inject into repository constructor |
| Test a DAO | |
| Test a Riverpod provider | |
| Test a widget | Set screen size, use |
| Test a loading state | Use |
| Test platform-specific UI | |
| Test GoRouter navigation | |
| 场景 | 核心模式 |
|---|---|
| 测试Repository | 模拟DAO + API → 注入到Repository构造函数 |
| 测试DAO | 在setUp中使用 |
| 测试Riverpod Provider | |
| 测试Widget | 设置屏幕尺寸,使用 |
| 测试加载状态 | 使用 |
| 测试平台特定UI | |
| 测试GoRouter导航 | |
Running Tests
运行测试
bash
flutter test --coverage # All tests with coverage
flutter test test/path/to/test.dart # Specific file
flutter test --plain-name "Given valid data" # Filter by name
genhtml coverage/lcov.info -o coverage/html # Generate HTML coverage reportbash
flutter test --coverage # 运行所有测试并生成覆盖率报告
flutter test test/path/to/test.dart # 运行指定文件的测试
flutter test --plain-name "Given valid data" # 按名称过滤测试
genhtml coverage/lcov.info -o coverage/html # 生成HTML格式的覆盖率报告Prefix any command with fvm
if using Flutter Version Manager
fvm如果使用Flutter版本管理器,在所有命令前添加fvm
前缀
fvmundefinedundefinedCommon Mistakes
常见错误
| Mistake | Fix |
|---|---|
| Mocking a provider directly | Override its dependencies: |
Missing | Tests pollute each other — always reset |
| Use |
| Finding widgets by text string | Use |
| No screen size in widget tests | Add |
Not resetting | Set to |
| Write |
| 错误 | 修复方案 |
|---|---|
| 直接模拟Provider | 覆盖其依赖项: |
| 测试会互相干扰 — 务必重置 |
测试中使用 | 改用 |
| 通过文本字符串查找Widget | 使用 |
| Widget测试中未设置屏幕尺寸 | 添加 |
未重置 | 测试结束后设置为 |
| 编写 |
Test Checklist
测试检查清单
Setup & Mocking:
- Dependencies mocked (not providers)
- SharedPreferences mocked if used
- in
GetIt.I.reset()tearDown - Streams closed in
tearDown - Controllers disposed in
tearDown
Widget Tests:
- Keys added to source widgets and used in
find.byKey() - Screen size set (+
physicalSize)devicePixelRatio - Platform overrides reset ()
debugDefaultTargetPlatformOverride = null - Navigation verified if applicable
Test Coverage:
- Success and failure paths covered
- Edge cases tested (null, empty, max values)
- Loading and error states tested
- Async handled correctly (no )
Future.delayed
Code Quality:
- Given-When-Then naming used
- or
verify()where appropriateverifyNever() - Tests are isolated and deterministic
配置与模拟:
- 依赖项已模拟(而非直接模拟Provider)
- 若使用SharedPreferences,已对其进行模拟
- 中包含
tearDownGetIt.I.reset() - 中已关闭流
tearDown - 中已销毁控制器
tearDown
Widget测试:
- 源Widget已添加Key,并在中使用
find.byKey() - 已设置屏幕尺寸(+
physicalSize)devicePixelRatio - 平台覆盖已重置()
debugDefaultTargetPlatformOverride = null - 若涉及导航,已验证导航行为
测试覆盖率:
- 已覆盖成功路径和失败路径
- 已测试边缘情况(null、空值、最大值)
- 已测试加载状态和错误状态
- 异步处理正确(未使用)
Future.delayed
代码质量:
- 使用了Given-When-Then命名方式
- 适当使用了或
verify()verifyNever() - 测试相互独立且结果可预测