概述
Flutter 提供了多种测试框架,适用于不同的测试场景。本文档详细对比各测试框架的特点、使用场景和最佳实践。
测试框架对比表
| 特性 | flutter_test | integration_test | flutter_driver (已弃用) |
|---|---|---|---|
| 测试类型 | 单元测试 / Widget 测试 | 集成测试 / E2E 测试 | E2E 测试 |
| 运行环境 | 测试虚拟机 (VM) | 真机 / 模拟器 | 真机 / 模拟器 |
| 执行速度 | ⚡ 极快 (毫秒级) | 🐢 较慢 (秒级) | 🐢 较慢 (秒级) |
| UI 渲染 | 虚拟渲染 (无真实屏幕) | 真实渲染 | 真实渲染 |
| 网络访问 | 需 Mock | ✅ 真实网络 | ✅ 真实网络 |
| 平台 API | 需 Mock | ✅ 真实平台 API | ✅ 真实平台 API |
| CI/CD 支持 | ✅ 优秀 | ✅ 良好 | ⚠️ 复杂 |
| 调试能力 | ✅ 完整 | ⚠️ 有限 | ❌ 困难 |
| 官方推荐 | ✅ 推荐 | ✅ 推荐 | ❌ 已弃用 |
1. flutter_test(单元测试 / Widget 测试)
简介
flutter_test 是 Flutter SDK 内置的测试框架,用于编写单元测试 和 Widget 测试。它在 Dart VM 中运行,不需要真实设备。
适用场景
- ✅ 业务逻辑测试(Controller、Service、Utils)
- ✅ 单个 Widget 的 UI 测试
- ✅ 状态管理测试(GetX、Riverpod、Bloc)
- ✅ 数据模型测试
- ✅ 纯 Dart 代码测试
项目配置
yaml
# pubspec.yaml
dev_dependencies:
flutter_test:
sdk: flutter
目录结构
bash
project/
├── lib/
│ └── src/
│ └── utils/
│ └── calculator.dart
└── test/ # 测试目录
└── utils/
└── calculator_test.dart # 以 _test.dart 结尾
代码示例
单元测试
dart
// test/utils/calculator_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/src/utils/calculator.dart';
void main() {
group('Calculator 测试', () {
late Calculator calculator;
setUp(() {
calculator = Calculator();
});
test('加法运算正确', () {
expect(calculator.add(2, 3), equals(5));
});
test('减法运算正确', () {
expect(calculator.subtract(5, 3), equals(2));
});
test('除以零抛出异常', () {
expect(
() => calculator.divide(10, 0),
throwsA(isA<ArgumentError>()),
);
});
});
}
Widget 测试
dart
// test/widgets/counter_widget_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/src/widgets/counter_widget.dart';
void main() {
group('CounterWidget 测试', () {
testWidgets('初始值为0', (WidgetTester tester) async {
await tester.pumpWidget(
const MaterialApp(home: CounterWidget()),
);
expect(find.text('0'), findsOneWidget);
});
testWidgets('点击加号按钮后数值增加', (WidgetTester tester) async {
await tester.pumpWidget(
const MaterialApp(home: CounterWidget()),
);
// 点击加号按钮
await tester.tap(find.byIcon(Icons.add));
await tester.pump(); // 触发重建
expect(find.text('1'), findsOneWidget);
});
testWidgets('长按按钮显示提示', (WidgetTester tester) async {
await tester.pumpWidget(
const MaterialApp(home: CounterWidget()),
);
await tester.longPress(find.byType(ElevatedButton));
await tester.pumpAndSettle(); // 等待动画完成
expect(find.text('长按提示'), findsOneWidget);
});
});
}
运行命令
bash
# 运行所有测试
flutter test
# 运行指定文件
flutter test test/utils/calculator_test.dart
# 运行指定目录
flutter test test/widgets/
# 生成覆盖率报告
flutter test --coverage
常用 API
| API | 说明 |
|---|---|
test() |
定义单个测试用例 |
testWidgets() |
定义 Widget 测试用例 |
group() |
测试分组 |
setUp() |
每个测试前执行 |
tearDown() |
每个测试后执行 |
setUpAll() |
所有测试前执行一次 |
tearDownAll() |
所有测试后执行一次 |
expect() |
断言 |
find.text() |
查找文本 Widget |
find.byType() |
按类型查找 Widget |
find.byKey() |
按 Key 查找 Widget |
find.byIcon() |
按图标查找 Widget |
tester.tap() |
模拟点击 |
tester.enterText() |
模拟输入文本 |
tester.drag() |
模拟拖动 |
tester.pump() |
触发帧重建 |
tester.pumpAndSettle() |
等待所有动画完成 |
2. integration_test(集成测试)
简介
integration_test 是 Flutter 官方推荐的集成测试/E2E 测试框架。它在真实设备或模拟器上运行完整的应用,可以测试完整的用户流程。
适用场景
- ✅ 端到端用户流程测试
- ✅ 需要真实网络请求的测试
- ✅ 需要平台特定功能的测试(相机、GPS、推送等)
- ✅ 多页面导航测试
- ✅ 性能测试
- ✅ 屏幕截图测试
项目配置
yaml
# pubspec.yaml
dev_dependencies:
flutter_test:
sdk: flutter
integration_test:
sdk: flutter
目录结构
bash
project/
├── lib/
│ └── main.dart
├── integration_test/ # 集成测试专用目录
│ ├── app_test.dart
│ └── login_flow_test.dart
└── test/
└── ... # 单元/Widget测试
代码示例
dart
// integration_test/login_flow_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:my_app/main.dart' as app;
/// 每步之间的延迟时间
const stepDelay = Duration(seconds: 2);
/// 延迟函数 - 等待指定时间并保持UI响应
Future<void> delay(WidgetTester tester, [Duration duration = stepDelay]) async {
await tester.pump(duration);
}
void main() {
// 初始化集成测试绑定(必须)
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('登录流程测试', () {
testWidgets('用户可以成功登录', (WidgetTester tester) async {
// 1. 启动应用
print('📱 步骤1: 启动应用...');
app.main();
await tester.pumpAndSettle();
await delay(tester);
// 2. 验证登录页面加载
print('📱 步骤2: 验证登录页面...');
expect(find.text('登录'), findsOneWidget);
await delay(tester);
// 3. 输入用户名
print('📱 步骤3: 输入用户名...');
final usernameField = find.byKey(const Key('username_field'));
await tester.enterText(usernameField, 'testuser');
await tester.pumpAndSettle();
await delay(tester);
// 4. 输入密码
print('📱 步骤4: 输入密码...');
final passwordField = find.byKey(const Key('password_field'));
await tester.enterText(passwordField, 'password123');
await tester.pumpAndSettle();
await delay(tester);
// 5. 点击登录按钮
print('📱 步骤5: 点击登录...');
final loginButton = find.byKey(const Key('login_button'));
await tester.tap(loginButton);
await tester.pumpAndSettle(const Duration(seconds: 5)); // 等待网络请求
await delay(tester);
// 6. 验证跳转到首页
print('📱 步骤6: 验证首页...');
expect(find.text('首页'), findsOneWidget);
await delay(tester);
print('✅ 测试通过:登录流程正常');
});
testWidgets('输入错误密码显示错误提示', (WidgetTester tester) async {
app.main();
await tester.pumpAndSettle();
// 输入用户名
await tester.enterText(
find.byKey(const Key('username_field')),
'testuser',
);
// 输入错误密码
await tester.enterText(
find.byKey(const Key('password_field')),
'wrong_password',
);
// 点击登录
await tester.tap(find.byKey(const Key('login_button')));
await tester.pumpAndSettle(const Duration(seconds: 3));
// 验证错误提示
expect(find.text('密码错误'), findsOneWidget);
});
});
}
运行命令
bash
# 在连接的设备/模拟器上运行
flutter test integration_test/login_flow_test.dart
# 指定设备运行
flutter test integration_test/ -d <device_id>
# 指定环境变量
flutter test integration_test/ --dart-define=ENV=dev
# 运行并生成报告
flutter test integration_test/ --reporter json > test_results.json
高级用法
截图测试
dart
testWidgets('首页UI截图', (WidgetTester tester) async {
final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();
app.main();
await tester.pumpAndSettle();
// 截图
await binding.takeScreenshot('home_page');
});
性能测试
dart
testWidgets('列表滚动性能', (WidgetTester tester) async {
final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();
app.main();
await tester.pumpAndSettle();
// 开始追踪
await binding.traceAction(() async {
// 滚动列表
await tester.fling(
find.byType(ListView),
const Offset(0, -500),
1000,
);
await tester.pumpAndSettle();
});
});
3. flutter_driver(已弃用)
⚠️ 重要提示
flutter_driver 已被 Flutter 官方弃用 ,推荐使用 integration_test 替代。
历史背景
flutter_driver 是早期的 E2E 测试框架,采用客户端-服务器架构:
- 测试脚本运行在主机(Host)
- 应用运行在设备上
- 通过 WebSocket 通信
为什么被弃用
- 架构复杂:需要两个进程协作
- 调试困难:无法直接访问 Widget 树
- 功能受限:只能通过有限的 Finder 查找元素
- 维护成本高 :需要同时维护
app.dart和测试脚本
迁移建议
如果项目中还在使用 flutter_driver,建议迁移到 integration_test:
| flutter_driver | integration_test |
|---|---|
driver.tap(find.byValueKey('key')) |
tester.tap(find.byKey(Key('key'))) |
driver.enterText(find, 'text') |
tester.enterText(find, 'text') |
driver.waitFor(find) |
expect(find, findsOneWidget) |
driver.getText(find) |
(tester.widget(find) as Text).data |
测试金字塔最佳实践
scss
╱╲
╱ ╲
╱ E2E╲ ← integration_test (10%)
╱──────╲ 完整用户流程
╱ ╲
╱ Widget ╲ ← flutter_test/testWidgets (20%)
╱────────────╲ 单个页面/组件
╱ ╲
╱ 单元测试 ╲ ← flutter_test/test (70%)
╱──────────────────╲ 业务逻辑/工具类
推荐比例
- 单元测试 (70%):覆盖所有业务逻辑
- Widget 测试 (20%):覆盖关键 UI 组件
- 集成测试 (10%):覆盖核心用户流程
项目测试目录结构
bash
project/
├── lib/
│ └── src/
│ ├── controllers/
│ ├── models/
│ ├── pages/
│ ├── services/
│ └── utils/
│
├── test/ # flutter_test
│ ├── controllers/ # Controller 测试
│ │ └── home_controller_test.dart
│ ├── models/ # Model 测试
│ │ └── user_model_test.dart
│ ├── services/ # Service 测试
│ │ └── api_service_test.dart
│ ├── utils/ # 工具类测试
│ │ └── date_util_test.dart
│ └── widgets/ # Widget 测试
│ └── custom_button_test.dart
│
├── integration_test/ # integration_test
│ ├── app_test.dart # 应用启动测试
│ ├── login_flow_test.dart # 登录流程测试
│ ├── chat_flow_test.dart # 聊天流程测试
│ └── meeting_flow_test.dart # 会议流程测试
│
└── pubspec.yaml
常见问题 FAQ
Q1: Widget 测试和集成测试有什么区别?
| 方面 | Widget 测试 | 集成测试 |
|---|---|---|
| 运行环境 | Dart VM | 真机/模拟器 |
| 网络请求 | 需要 Mock | 真实请求 |
| 执行速度 | 快 | 慢 |
| 覆盖范围 | 单个 Widget | 完整应用 |
Q2: 何时使用 pump() vs pumpAndSettle()?
pump(): 触发一帧重建,用于简单状态更新pumpAndSettle(): 等待所有动画完成,用于有动画的场景
dart
// 简单点击
await tester.tap(find.byType(ElevatedButton));
await tester.pump();
// 有动画的操作(页面跳转、对话框等)
await tester.tap(find.text('打开对话框'));
await tester.pumpAndSettle();
Q3: 集成测试如何处理登录状态?
dart
// 方法1: 通过 UI 登录
testWidgets('需要登录的测试', (tester) async {
app.main();
await tester.pumpAndSettle();
// 执行登录流程
await _performLogin(tester);
// 继续测试
// ...
});
// 方法2: 预设登录状态(推荐)
testWidgets('需要登录的测试', (tester) async {
// 设置测试用的登录凭证
await SharedPreferences.setMockInitialValues({
'token': 'test_token',
'userId': 'test_user',
});
app.main();
await tester.pumpAndSettle();
// 直接测试需要登录的功能
// ...
});
Q4: 如何在 CI/CD 中运行测试?
yaml
# .github/workflows/test.yml
name: Flutter Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: subosito/flutter-action@v2
with:
flutter-version: '3.16.5'
# 单元测试和 Widget 测试
- name: Run unit tests
run: flutter test --coverage
# 集成测试(需要模拟器)
- name: Run integration tests
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 29
script: flutter test integration_test/
总结
| 场景 | 推荐框架 |
|---|---|
| 测试纯 Dart 逻辑 | flutter_test + test() |
| 测试单个 Widget | flutter_test + testWidgets() |
| 测试完整用户流程 | integration_test |
| 测试需要真实网络的功能 | integration_test |
| 测试平台特定功能 | integration_test |
| 性能测试 | integration_test |
参考资料
文档更新日期: 2026-01-07