跳到主要内容

测试

测试是保证应用质量的重要手段。本章将介绍 Flutter 中的单元测试、Widget 测试和集成测试。

单元测试

单元测试用于测试独立的函数、方法或类。

基本结构

// test/counter_test.dart
import 'package:flutter_test/flutter_test.dart';

void main() {
test('计数器应该从0开始', () {
final counter = Counter();
expect(counter.value, 0);
});

test('计数器递增', () {
final counter = Counter();
counter.increment();
expect(counter.value, 1);
});

test('计数器递减', () {
final counter = Counter();
counter.decrement();
expect(counter.value, -1);
});
}

测试类

// lib/counter.dart
class Counter {
int _value = 0;

int get value => _value;

void increment() => _value++;
void decrement() => _value--;
void reset() => _value = 0;
}

分组测试

void main() {
group('Counter', () {
late Counter counter;

setUp(() {
counter = Counter();
});

test('初始值为0', () {
expect(counter.value, 0);
});

test('递增', () {
counter.increment();
expect(counter.value, 1);
});

test('递减', () {
counter.decrement();
expect(counter.value, -1);
});

test('重置', () {
counter.increment();
counter.increment();
counter.reset();
expect(counter.value, 0);
});
});
}

异步测试

void main() {
test('异步获取数据', () async {
final service = DataService();
final data = await service.fetchData();
expect(data, isNotEmpty);
});

test('Stream 测试', () async {
final stream = Stream.fromIterable([1, 2, 3]);

await expectLater(
stream,
emitsInOrder([1, 2, 3, emitsDone]),
);
});
}

Mock 测试

使用 mockito 进行 Mock 测试:

dev_dependencies:
mockito: ^5.0.0
build_runner: ^2.0.0
import 'package:mockito/mockito.dart';
import 'package:mockito/annotations.dart';

@GenerateMocks([ApiService])
import 'user_repository_test.mocks.dart';

void main() {
late MockApiService mockApiService;
late UserRepository userRepository;

setUp(() {
mockApiService = MockApiService();
userRepository = UserRepository(mockApiService);
});

test('获取用户列表', () async {
when(mockApiService.getUsers()).thenAnswer(
(_) async => [User(id: 1, name: '张三')],
);

final users = await userRepository.getUsers();

expect(users, hasLength(1));
expect(users.first.name, '张三');
verify(mockApiService.getUsers()).called(1);
});

test('获取用户失败', () async {
when(mockApiService.getUsers()).thenThrow(Exception('网络错误'));

expect(
() => userRepository.getUsers(),
throwsException,
);
});
}

运行代码生成:

flutter pub run build_runner build

Widget 测试

Widget 测试用于测试 UI 组件的行为。

基本 Widget 测试

// test/counter_widget_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/counter_page.dart';

void main() {
testWidgets('计数器页面测试', (WidgetTester tester) async {
// 构建 Widget
await tester.pumpWidget(MaterialApp(home: CounterPage()));

// 验证初始状态
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);

// 点击递增按钮
await tester.tap(find.byIcon(Icons.add));
await tester.pump(); // 重新构建

// 验证状态变化
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);
});
}

查找器(Finders)

// 按文本查找
find.text('确定')

// 按类型查找
find.byType(ElevatedButton)

// 按 Key 查找
find.byKey(ValueKey('submit_button'))

// 按图标查找
find.byIcon(Icons.add)

// 按 Widget 查找
find.byWidget(MyWidget())

// 查找后代
find.descendant(
of: find.byType(Row),
matching: find.text('项目'),
)

// 查找祖先
find.ancestor(
of: find.text('标题'),
matching: find.byType(AppBar),
)

操作

// 点击
await tester.tap(find.text('按钮'));
await tester.pump();

// 输入文本
await tester.enterText(find.byType(TextField), 'Hello');
await tester.pump();

// 滚动
await tester.scrollUntilVisible(
find.text('项目 100'),
100.0,
);
await tester.pump();

// 长按
await tester.longPress(find.text('项目'));
await tester.pump();

// 拖拽
await tester.drag(find.byType(Slider), Offset(100, 0));
await tester.pump();

测试动画

testWidgets('动画测试', (WidgetTester tester) async {
await tester.pumpWidget(MaterialApp(home: AnimationPage()));

// 触发动画
await tester.tap(find.text('播放'));

// 等待动画完成
await tester.pumpAndSettle();

// 验证最终状态
expect(find.text('完成'), findsOneWidget);
});

// 测试特定时间点的动画
testWidgets('动画中间状态', (WidgetTester tester) async {
await tester.pumpWidget(MaterialApp(home: AnimationPage()));

await tester.tap(find.text('播放'));
await tester.pump(Duration(milliseconds: 500)); // 动画中间

// 验证中间状态
expect(find.byType(Transform), findsOneWidget);
});

集成测试

集成测试用于测试完整的应用流程。

添加依赖

dev_dependencies:
integration_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' as app;

void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();

testWidgets('完整登录流程', (WidgetTester tester) async {
app.main();
await tester.pumpAndSettle();

// 输入邮箱
await tester.enterText(
find.byKey(Key('email_field')),
'[email protected]',
);

// 输入密码
await tester.enterText(
find.byKey(Key('password_field')),
'password123',
);

// 点击登录
await tester.tap(find.text('登录'));
await tester.pumpAndSettle();

// 验证登录成功
expect(find.text('欢迎'), findsOneWidget);
});

testWidgets('添加和删除待办', (WidgetTester tester) async {
app.main();
await tester.pumpAndSettle();

// 添加待办
await tester.tap(find.byIcon(Icons.add));
await tester.pumpAndSettle();

await tester.enterText(find.byType(TextField), '测试待办');
await tester.tap(find.text('保存'));
await tester.pumpAndSettle();

// 验证待办已添加
expect(find.text('测试待办'), findsOneWidget);

// 删除待办
await tester.drag(find.text('测试待办'), Offset(-500, 0));
await tester.pumpAndSettle();

// 验证待办已删除
expect(find.text('测试待办'), findsNothing);
});
}

运行集成测试:

flutter test integration_test/app_test.dart

测试覆盖率

生成测试覆盖率报告:

flutter test --coverage
genhtml coverage/lcov.info -o coverage/html
open coverage/html/index.html

最佳实践

1. 测试组织

test/
├── unit/
│ ├── counter_test.dart
│ └── user_repository_test.dart
├── widget/
│ ├── counter_page_test.dart
│ └── login_page_test.dart
└── helpers/
└── test_helpers.dart

2. 测试辅助函数

// test/helpers/test_helpers.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

Widget makeTestable(Widget child) {
return MaterialApp(home: child);
}

Widget makeTestableWithScaffold(Widget child) {
return MaterialApp(
home: Scaffold(body: child),
);
}

3. 命名规范

// 好的命名
test('当点击按钮时计数器应该递增', () { ... });
test('输入无效邮箱时应该显示错误信息', () { ... });

// 不好的命名
test('测试1', () { ... });
test('计数器', () { ... });

小结

本章我们学习了:

  1. 单元测试:测试独立的函数和类
  2. Widget 测试:测试 UI 组件
  3. 集成测试:测试完整应用流程
  4. Mock 测试:模拟依赖
  5. 测试覆盖率:衡量测试质量
  6. 最佳实践:组织和命名规范

练习

  1. 为计数器应用编写完整的单元测试
  2. 为登录页面编写 Widget 测试
  3. 为购物车功能编写集成测试
  4. 使用 Mock 测试 API 服务