关于 testing
为 Flutter 和 Dart 开发者提供编写、评审测试的指南,强调测试应能发现真实回归,并涵盖单元测试、Widget 测试及测试组织。
主要功能
- 01检查测试能否发现真实问题
- 02按类组织测试并命名用例
- 03区分单元测试与 Widget 测试
- 04编写 Widget 测试
- 05遵循 mocktail 模拟规范
- 06镜像源代码目录组织测试文件
适用场景
- 01为业务逻辑和仓库编写单元测试
- 02验证 Widget 渲染与交互
- 03审查测试的有效性与覆盖
- 04排查不稳定或误报的测试
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ evanca/ flutter-ai-rules --skill testing安装后在 Claude Code 中输入/testing确认已加载。
详细介绍
测试技能
编写有效、有意义的 Flutter 和 Dart 测试,捕获真正的回归问题。
适用场景
在以下情况下使用此技能:
- 为业务逻辑、仓库或实用工具类编写单元测试。
- 为 UI 组件编写组件测试。
- 检查现有测试的正确性和覆盖率。
- 修复不稳定或误报的测试。
- 决定使用单元测试、组件测试还是集成测试。
1. 测试有效性
编写或接受测试之前,先问:
“如果真实代码损坏,这个测试真的会失败吗?”
- 避免只验证模拟或伪造行为、却未执行真实逻辑的测试。
- 避免验证语言或标准库保证的行为。
- 每项测试都必须能够捕获真正的回归问题。
// BAD — tests the mock, not real logic
test('should return user', () {
when(() => repo.getUser()).thenReturn(fakeUser);
expect(repo.getUser(), fakeUser); // Only proves the mock works
});
// GOOD — tests the cubit's state transitions driven by the mock
blocTest<UserCubit, UserState>(
'should emit loaded state when getUser succeeds',
build: () {
when(() => repo.getUser()).thenAnswer((_) async => fakeUser);
return UserCubit(repo);
},
act: (cubit) => cubit.fetchUser(),
expect: () => [
const UserState(status: UserStatus.loading),
UserState(status: UserStatus.loaded, user: fakeUser),
],
);
2. 结构
测试文件中始终使用 group()。组名应为被测类的名称:
group('Counter', () {
late Counter counter;
setUp(() {
counter = Counter();
});
test('value should start at 0', () {
expect(counter.value, 0);
});
test('should increment value by 1', () {
counter.increment();
expect(counter.value, 1);
});
});
规则:
- 使用
setUp创建共享对象;使用tearDown进行清理(关闭流、控制器)。 - 每项测试只关注一种行为。
- 如果一个类有许多方法,请为子功能嵌套
group()块。
3. 命名
使用 “should” 描述预期行为,为测试用例命名:
test('should emit updated list when item is added', () { ... });
test('should throw ArgumentError when input is negative', () { ... });
4. 单元测试与组件测试
| 类型 | 测试目标 | 工具 |
|---|---|---|
| 单元测试 | 纯 Dart 逻辑、仓库、cubit/bloc | test、bloc_test、mocktail |
| 组件测试 | 单个组件、UI 行为、导航 | flutter_test、WidgetTester |
业务逻辑默认使用单元测试。验证 UI 渲染、手势处理或组件交互时,使用组件测试。
5. 组件测试模式
testWidgets('should display error message on failure', (tester) async {
await tester.pumpWidget(
MaterialApp(
home: BlocProvider<LoginCubit>.value(
value: mockLoginCubit,
child: const LoginView(),
),
),
);
// Simulate failure state
whenListen(
mockLoginCubit,
Stream.fromIterable([const LoginState(status: LoginStatus.failure, errorMessage: 'Invalid')]),
initialState: const LoginState(),
);
await tester.pump();
expect(find.text('Invalid'), findsOneWidget);
});
规则:
- 将组件包裹在
MaterialApp(或应用的根组件)中,以提供MediaQuery、Directionality等内容。 - 单帧使用
pump();需要动画播放完毕时使用pumpAndSettle()。 - 对可能包含本地化或动态文本的组件,优先使用
find.byKey,而不是find.text。
6. 模拟最佳实践
- 使用
mocktail创建模拟对象(无需代码生成)。 - 对传递给
any()的自定义类型,在setUpAll中调用registerFallbackValue()。 - 在仓库边界进行模拟,而不是在 HTTP/数据库层进行模拟。
class MockAuthRepository extends Mock implements AuthRepository {}
void main() {
setUpAll(() {
registerFallbackValue(FakeLoginRequest());
});
// ... tests
}
7. 测试文件组织
test/
feature_a/
cubit/
feature_a_cubit_test.dart
view/
feature_a_view_test.dart
model/
feature_a_model_test.dart
- 在
test/下镜像lib/文件夹结构。 - 每个源文件对应一个测试文件。
- 将测试文件命名为
<source_file>_test.dart。
评论
支持 Markdown