# 概念
Flutter 官方介绍的测试主要分为 三种,分别是unit test, widget test, integration test.
- Unit testing (opens new window), 即单元测试。用于测试一个单独的函数,方法或类。单元测试通常不会涉及读取或写入硬盘数据、页面渲染、或者接收用户动作。常用库:
test
- Widget testing (opens new window), 即组件测试。用于测试组件、页面内容。这个过程通常会涉及到多个类,并且与组件生命周期上下文相关。相对于单元测试,这个过程会涉及到用户交互、页面渲染,组件实例化等过程。 常用库:
flutter_test
- Integration testing (opens new window),即集成测试。用于验证所有组件和服务都按照预期运行。另外也可以用于测试应用的性能表现。
在flutter测试中,涉及到请求相关内容,可以使用Mockito
库来创建mock数据。
# Widget Test 示例
# flutter_test API
以下为示例代码用到的部分API
testWidgets(String description, WidgetTesterCallback callback, {....})
。testWidgets
(opens new window)接收两个必选参数以及多个可选参数,第一个为description
,即测试描述。第二个参数为WidgetTesterCallback 测试函数回调,该回调带有一个WidgetTester
类参数。expect(dynamic actual, matcher, String? reason, dynamic skip, // true or a String})
,expect (opens new window)是flutter_test
的断言方法。 用于断言actual
是否匹配matcher
- Matcher类 (opens new window)提供一些匹配参数,通常用于断言的判断条件。
findsOneWidget
(opens new window), 用于断言Finder
类在组件树中定位了一个组件。findsNothing
(opens new window), 用于断言Finder
类未定位到组件。findsNWidgets(n)
(opens new window), 用于断言Finder
类定位到了n个组件。- ....
Finder类
(opens new window),查找组件树并返回符合对应pattern的节点。find.text
用于查找test的组件find.widgetWithText
用于查找包含置顶文字的、对应类型的组件
WidgetTester类
(opens new window),一个与测试环境及组件交互的类。该类的实例可以被用为AnimationController
对象的vsync
参数。WidgetTester.pump
相关方法,用于触发页面渲染帧WidgetTester.enterText
,用于在Finder
查询到的实例中输入内容WidgetTester.tap
,用于单击组件进行交互- .....
更多详细API见flutter_test (opens new window)
# 登录页测试
# 初始化
// 解决由于 MediaQuery 没有父元素的报错问题。需要将组件用MaterialApp包裹起来
// https://stackoverflow.com/questions/48498709/widget-test-fails-with-no-mediaquery-widget-found
Widget createWidgetForTesting({Widget child}){
return MaterialApp(
home: child,
);
}
// 一些初始化调用
init(tester) async {
// 有些初始化内容在MyApp中调用, 所以在这里直接调用了MyApp
await tester.pumpWidget(MyApp());
await tester.pumpWidget(createWidgetForTesting(child: new LoginPage())); // 打开登录页
await tester.pumpAndSettle(); // 等待一帧
}
# 渲染
对于登录页,页面主要包含两个输入组件,一个登录按钮,以及忘记密码和注册新用户按钮。
testWidgets('页面渲染成功', (WidgetTester tester) async {
await init(tester); //
expect(find.text('请输入手机号'), findsOneWidget);
expect(find.text('请输入密码'), findsOneWidget);
expect(find.text('忘记密码?'), findsOneWidget);
expect(find.text('注册新用户'), findsOneWidget);
expect(find.text("登录"), findsOneWidget);
});
# 输入测试
表单的输入内容大体类似,所以先对输入内容测试做一个简单封装,方便在testWidgets中批量设置测试用例进行调用。
// TextFormField输入格式测试封装
testTextFormField(tester, widgetText, input, expectFunc) async {
final Finder inputWidget = find.widgetWithText(TextFormField, widgetText); // 查找输入组件
await tester.enterText(inputWidget, input);
await tester.tap(submit); // 点击登录按钮
await tester.pumpAndSettle();
expectFunc();
}
对手机号和密码输入框,各自进行输入为空、输入格式错误的用例进行测试
testWidgets('输入格式验证成功', (WidgetTester tester) async {
await init(tester);
// 不输入内容点击登录
await tester.tap(submit);
await tester.pumpAndSettle();
expect(find.text('手机号格式错误'), findsOneWidget);
expect(find.text('密码不能为空'), findsOneWidget);
// 输入测试用例并点击登录
List phoneTests = ['1234567899', '123456789966']; // 手机格式测试用例
for (var phone in phoneTests) {
await testTextFormField(tester, "请输入手机号", phone, ()=>expect(find.text('手机号格式错误'), findsOneWidget));
}
List pwdTestes = ['12345', '1234578901234567890123456789012315']; // 密码格式测试用例
for (var pwd in pwdTestes) {
await testTextFormField(tester, "请输入密码", pwd, ()=>expect(find.text('密码为6到32位字符'), findsOneWidget));
}
}
# 整体代码
// login_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'LoginPage.dart';
import 'main.dart';
void main() {
final Finder submit = find.widgetWithText(TextButton, '登录');
// 解决由于MediaQuery 没有父元素的报错问题。需要将元素用MaterialApp包裹起来
Widget createWidgetForTesting({Widget child}){
return MaterialApp(
home: child,
);
}
// 一些初始化调用
init(tester) async {
// 有些初始化内容在MyApp中调用, 所以在这里直接运行了main.dart
await tester.pumpWidget(MyApp());
await tester.pumpWidget(createWidgetForTesting(child: new LoginPage()));
await tester.pumpAndSettle();
}
testWidgets('页面渲染成功', (WidgetTester tester) async {
await init(tester);
expect(find.text('登录xx账号'), findsOneWidget);
expect(find.text('请输入手机号'), findsOneWidget);
expect(find.text('请输入密码'), findsOneWidget);
expect(find.text('忘记密码?'), findsOneWidget);
expect(find.text('注册新用户'), findsOneWidget);
expect(find.text("登录"), findsOneWidget);
});
// TextFormField输入格式测试
testTextFormField(tester, widgetText, input, expectFunc) async {
final Finder inputWidget = find.widgetWithText(TextFormField, widgetText); // 查找输入组件
await tester.enterText(inputWidget, input);
await tester.tap(submit); // 点击登录按钮
await tester.pumpAndSettle();
expectFunc();
}
testWidgets('输入格式验证成功', (WidgetTester tester) async {
await init(tester);
await tester.tap(submit);
await tester.pumpAndSettle();
expect(find.text('手机号格式错误'), findsOneWidget);
expect(find.text('密码不能为空'), findsOneWidget);
List phoneTests = ['1234567899', '123456789966']; // 手机格式测试用例
for (var phone in phoneTests) {
await testTextFormField(tester, "请输入手机号", phone, ()=>expect(find.text('手机号格式错误'), findsOneWidget));
}
List pwdTestes = ['12345', '1234578901234567890123456789012315']; // 密码格式测试用例
for (var pwd in pwdTestes) {
await testTextFormField(tester, "请输入密码", pwd, ()=>expect(find.text('密码为6到32位字符'), findsOneWidget));
}
}
}