简介
集成测试用于测试应用的完整功能流程。本文介绍如何在 Flutter 中编写集成测试。
基本配置
添加依赖
dev_dependencies:
integration_test:
sdk: flutter
flutter_test:
sdk: flutter
创建测试文件
// integration_test/app_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:my_app/main.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('Complete login flow test', (tester) async {
await tester.pumpWidget(MyApp());
await tester.pumpAndSettle();
// 测试登录流程
});
}
测试方法
页面交互
testWidgets('Login flow', (tester) async {
await tester.pumpWidget(MyApp());
await tester.pumpAndSettle();
// 输入用户名
await tester.enterText(
find.byKey(Key('username_field')),
'testuser',
);
// 输入密码
await tester.enterText(
find.byKey(Key('password_field')),
'password123',
);
// 点击登录按钮
await tester.tap(find.byKey(Key('login_button')));
await tester.pumpAndSettle();
// 验证登录成功
expect(find.text('Welcome, testuser'), findsOneWidget);
});
网络请求
testWidgets('API integration test', (tester) async {
await tester.pumpWidget(MyApp());
// 触发数据加载
await tester.tap(find.byKey(Key('load_data_button')));
await tester.pumpAndSettle();
// 验证加载状态
expect(find.byType(CircularProgressIndicator), findsOneWidget);
// 等待数据加载完成
await tester.pumpAndSettle(Duration(seconds: 3));
// 验证数据显示
expect(find.byType(ListTile), findsNWidgets(10));
});
完整示例
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('End-to-end test', () {
testWidgets('Complete user flow', (tester) async {
// 启动应用
await tester.pumpWidget(MyApp());
await tester.pumpAndSettle();
// 1. 登录流程
await _loginFlow(tester);
// 2. 创建新帖子
await _createPostFlow(tester);
// 3. 查看帖子列表
await _viewPostsFlow(tester);
// 4. 编辑个人资料
await _editProfileFlow(tester);
// 5. 退出登录
await _logoutFlow(tester);
});
});
}
Future<void> _loginFlow(WidgetTester tester) async {
// 输入登录信息
await tester.enterText(
find.byKey(Key('email_field')),
'[email protected]',
);
await tester.enterText(
find.byKey(Key('password_field')),
'password123',
);
// 点击登录按钮
await tester.tap(find.byKey(Key('login_button')));
await tester.pumpAndSettle();
// 验证登录成功
expect(find.text('Welcome'), findsOneWidget);
}
Future<void> _createPostFlow(WidgetTester tester) async {
// 点击创建按钮
await tester.tap(find.byKey(Key('create_post_button')));
await tester.pumpAndSettle();
// 输入帖子内容
await tester.enterText(
find.byKey(Key('post_title_field')),
'Test Post Title',
);
await tester.enterText(
find.byKey(Key('post_content_field')),
'Test post content',
);
// 提交帖子
await tester.tap(find.byKey(Key('submit_post_button')));
await tester.pumpAndSettle();
// 验证帖子创建成功
expect(find.text('Post created successfully'), findsOneWidget);
}
Future<void> _viewPostsFlow(WidgetTester tester) async {
// 滚动列表
await tester.fling(
find.byType(ListView),
Offset(0, -500),
3000,
);
await tester.pumpAndSettle();
// 点击帖子
await tester.tap(find.text('Test Post Title'));
await tester.pumpAndSettle();
// 验证帖子详情
expect(find.text('Test post content'), findsOneWidget);
}
Future<void> _editProfileFlow(WidgetTester tester) async {
// 导航到个人资料页
await tester.tap(find.byKey(Key('profile_button')));
await tester.pumpAndSettle();
// 点击编辑按钮
await tester.tap(find.byKey(Key('edit_profile_button')));
await tester.pumpAndSettle();
// 更新个人信息
await tester.enterText(
find.byKey(Key('name_field')),
'Updated Name',
);
// 保存更改
await tester.tap(find.byKey(Key('save_profile_button')));
await tester.pumpAndSettle();
// 验证更新成功
expect(find.text('Profile updated'), findsOneWidget);
}
Future<void> _logoutFlow(WidgetTester tester) async {
// 点击退出按钮
await tester.tap(find.byKey(Key('logout_button')));
await tester.pumpAndSettle();
// 确认退出
await tester.tap(find.text('Yes'));
await tester.pumpAndSettle();
// 验证返回登录页
expect(find.byKey(Key('login_button')), findsOneWidget);
}
性能测试
测量启动时间
testWidgets('Measure app startup time', (tester) async {
final stopwatch = Stopwatch()..start();
await tester.pumpWidget(MyApp());
await tester.pumpAndSettle();
stopwatch.stop();
print('App startup time: ${stopwatch.elapsedMilliseconds}ms');
});
测量页面切换性能
testWidgets('Measure page transition performance', (tester) async {
await tester.pumpWidget(MyApp());
await tester.pumpAndSettle();
final stopwatch = Stopwatch()..start();
// 触发页面切换
await tester.tap(find.byKey(Key('next_page_button')));
await tester.pumpAndSettle();
stopwatch.stop();
print('Page transition time: ${stopwatch.elapsedMilliseconds}ms');
});
最佳实践
- 只覆盖核心流程:登录、下单、支付这 3 条(实战案例里说的)。集成测试跑一次几分钟,全量回归 20 条流程就是半小时——贪多等于没人愿意跑。
- 一个用例一条完整用户路径:
_loginFlow→_createPostFlow→…拆成 helper 函数(正文完整示例就是这么组织的),主用例读起来是用户故事,挂了能直接定位到哪一步。 - 测试数据要自包含:用固定测试账号(如 [email protected]/password123),用例开头保证数据状态(没有就创建),结尾清理。依赖”数据库里已经有条帖子”的测试,换个环境就挂。
- 等待用
pumpAndSettle而不是固定 sleep:正文网络请求示例里pumpAndSettle(Duration(seconds: 3))是反面教材——网络快了白等 3 秒,慢了 3 秒不够。等目标元素出现(waitFor)才是正解。 - 失败时截图:integration_test 挂掉时把当前屏幕 dump 出来。CI 上”expect 失败”配一张截图,定位速度差一个数量级——不然你只能对着日志猜。
注意事项
- 跑在真机/模拟器上,不是
flutter test:integration_test 需要flutter test integration_test起设备(正文基本配置已写)。CI 要配 macOS runner 跑模拟器或接真机农场——这正是小团队没写集成测试的真实原因(实战案例)。 - 超时设置要宽容:真机启动+网络请求,一个用例给 1-2 分钟超时。CI 机器性能波动大,按本地最快速度设超时等于定期误报。
- 别在集成测试里测分支:20 种表单校验组合是单元测试的活(见单元测试篇)。集成测试只走主线 happy path + 一条错误路径(如登录失败),分支覆盖靠下层测试。
- 设备兼容性抽样就行:不需要 20 台手机全量跑——选一台低端机(千元机)+ 一台主流机。低端机负责暴露性能问题和布局溢出,主流机负责功能。
- 网络要可控:连真实后端测的是”端到端”,但 CI 应该打 staging 环境。直连生产等于用测试账号污染真实数据——删不掉的那种。
实战案例
集成测试(integration_test,真机上跑完整流程)我还没写过。最接近的是 电商奢侈品专区改版 的 widget 测试——但它跑在 flutter test 里,是伪造环境,不是真机。没写的原因很实际:集成测试要起模拟器/真机,CI 配置复杂,跑一次几分钟,小团队的迭代速度受不了。我的替代方案是”关键流程人工走查清单”:发版前按 checklist 点一遍(登录→下单→支付→退出),15 分钟,比写集成测试便宜。当然这是妥协——用户量上来后,核心流程(登录、支付)一定要补集成测试,人工走查迟早会漏。参考:Flutter 官方文档 integration_test 章节。
总结
集成测试是确保应用整体功能正确性的重要手段。通过编写全面的集成测试,可以提前发现问题,提高应用质量。
