flutter-tester

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Flutter Tester

Flutter 测试实践指南

Requirements

前提条件

  • Flutter project with
    flutter_test
    dependency
  • Works with Riverpod, Mockito, and GetIt
  • Run
    dart run build_runner build
    to generate mocks after adding
    @GenerateMocks
    annotations
  • Compatible with FVM (
    fvm flutter test
    instead of
    flutter test
    )
  • 包含
    flutter_test
    依赖的Flutter项目
  • 兼容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 testingReference file
Repository, DAO, Service logic
references/layer_testing_patterns.md
Widget UI, interactions, dialogs, navigation
references/widget_testing_guide.md
Riverpod provider state, mutations, lifecycle
references/riverpod_testing_guide.md
根据测试内容加载对应的参考文件:
测试内容参考文件
Repository、DAO、Service逻辑
references/layer_testing_patterns.md
Widget界面、交互、弹窗、导航
references/widget_testing_guide.md
Riverpod Provider状态、变更、生命周期
references/riverpod_testing_guide.md

Core Principles

核心原则

1. Layer Isolation

1. 分层隔离

Test each layer against its own mocked dependencies:
LayerWhat to testWhat to mock
RepositoryData coordination between sourcesDAOs, APIs, Logger
DAODatabase CRUD operationsUse real in-memory DB, mock Logger
ProviderState management and transitionsServices, Repositories
ServiceBusiness logic and workflowsRepositories, Network clients
WidgetUI behaviour and interactionsProvider dependencies (via overrides)
针对每个分层,使用模拟的依赖项进行测试:
分层测试内容模拟对象
Repository多数据源间的数据协调逻辑DAO、API、Logger
DAO数据库CRUD操作使用真实内存数据库,模拟Logger
Provider状态管理与状态转换Service、Repository
Service业务逻辑与工作流Repository、网络客户端
WidgetUI行为与交互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
dart run build_runner build
after modifying
@GenerateMocks
.
dart
([IUserDAO, IUserAPI, ILogger])
void main() { ... }
修改
@GenerateMocks
后,运行
dart run build_runner build

Register with GetIt

注册到GetIt

dart
setUp(() {
  mockDAO = MockIUserDAO();
  mockLogger = MockILogger();
  GetIt.I
    ..registerSingleton<IUserDAO>(mockDAO)
    ..registerSingleton<ILogger>(mockLogger);
});

tearDown(() => GetIt.I.reset()); // Critical — always reset
dart
setUp(() {
  mockDAO = MockIUserDAO();
  mockLogger = MockILogger();
  GetIt.I
    ..registerSingleton<IUserDAO>(mockDAO)
    ..registerSingleton<ILogger>(mockLogger);
});

tearDown(() => GetIt.I.reset()); // 关键步骤 — 务必重置

Fakes vs Mocks

假实现(Fakes)与模拟对象(Mocks)

  • Fakes (
    class FakeLogger extends ILogger
    ) — silent stubs; use when you don't need to verify calls
  • Mocks (
    MockILogger
    ) — use when you need
    when()
    ,
    verify()
    , or
    thenThrow()
  • Fakes (
    class FakeLogger extends ILogger
    )—— 静默桩;无需验证调用时使用
  • Mocks (
    MockILogger
    )—— 需要使用
    when()
    verify()
    thenThrow()
    时使用

Quick Reference

快速参考

ScenarioKey pattern
Test a repositoryMock DAO + API → inject into repository constructor
Test a DAO
FakeDatabase
or
openInMemoryDatabase()
in setUp, delete table in tearDown
Test a Riverpod provider
createContainer(overrides: [serviceProvider.overrideWith(...)])
Test a widgetSet screen size, use
find.byKey()
, call
pumpAndSettle()
Test a loading stateUse
Completer
,
pump()
to assert loading, complete,
pump()
again
Test platform-specific UI
debugDefaultTargetPlatformOverride = TargetPlatform.iOS
— reset after
Test GoRouter navigation
FakeGoRouter
+
MockGoRouterProvider
场景核心模式
测试Repository模拟DAO + API → 注入到Repository构造函数
测试DAO在setUp中使用
FakeDatabase
openInMemoryDatabase()
,在tearDown中删除表
测试Riverpod Provider
createContainer(overrides: [serviceProvider.overrideWith(...)])
测试Widget设置屏幕尺寸,使用
find.byKey()
,调用
pumpAndSettle()
测试加载状态使用
Completer
,调用
pump()
断言加载状态,完成后再次调用
pump()
测试平台特定UI
debugDefaultTargetPlatformOverride = TargetPlatform.iOS
— 测试后重置
测试GoRouter导航
FakeGoRouter
+
MockGoRouterProvider

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 report
bash
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

如果使用Flutter版本管理器,在所有命令前添加
fvm
前缀

undefined
undefined

Common Mistakes

常见错误

MistakeFix
Mocking a provider directlyOverride its dependencies:
provider.overrideWith(...)
Missing
GetIt.I.reset()
in
tearDown
Tests pollute each other — always reset
await Future.delayed()
in tests
Use
await tester.pumpAndSettle()
or
Completer
instead
Finding widgets by text stringUse
find.byKey(const Key('name'))
— stable across text changes
No screen size in widget testsAdd
tester.view.physicalSize = const Size(1000, 1000)
Not resetting
debugDefaultTargetPlatformOverride
Set to
null
at the end of the test
tearDown()
without a lambda
Write
tearDown(() async { ... })
not
tearDown() async { ... }
错误修复方案
直接模拟Provider覆盖其依赖项:
provider.overrideWith(...)
tearDown
中缺少
GetIt.I.reset()
测试会互相干扰 — 务必重置
测试中使用
await Future.delayed()
改用
await tester.pumpAndSettle()
Completer
通过文本字符串查找Widget使用
find.byKey(const Key('name'))
— 不受文本变更影响
Widget测试中未设置屏幕尺寸添加
tester.view.physicalSize = const Size(1000, 1000)
未重置
debugDefaultTargetPlatformOverride
测试结束后设置为
null
tearDown()
未使用lambda表达式
编写
tearDown(() async { ... })
而非
tearDown() async { ... }

Test Checklist

测试检查清单

Setup & Mocking:
  • Dependencies mocked (not providers)
  • SharedPreferences mocked if used
  • GetIt.I.reset()
    in
    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
  • verify()
    or
    verifyNever()
    where appropriate
  • Tests are isolated and deterministic
配置与模拟:
  • 依赖项已模拟(而非直接模拟Provider)
  • 若使用SharedPreferences,已对其进行模拟
  • tearDown
    中包含
    GetIt.I.reset()
  • tearDown
    中已关闭流
  • tearDown
    中已销毁控制器
Widget测试:
  • 源Widget已添加Key,并在
    find.byKey()
    中使用
  • 已设置屏幕尺寸(
    physicalSize
    +
    devicePixelRatio
  • 平台覆盖已重置(
    debugDefaultTargetPlatformOverride = null
  • 若涉及导航,已验证导航行为
测试覆盖率:
  • 已覆盖成功路径和失败路径
  • 已测试边缘情况(null、空值、最大值)
  • 已测试加载状态和错误状态
  • 异步处理正确(未使用
    Future.delayed
代码质量:
  • 使用了Given-When-Then命名方式
  • 适当使用了
    verify()
    verifyNever()
  • 测试相互独立且结果可预测