App中的功能越来越多越复杂的时候,一些基本的功能的测试就可以交给Flutter提供的自动化测试来完成这些繁琐的工作。
Flutter中提供的三种测试:
- 单元测试:测试单一功能、方法或类。
- Widget 测试:(在其它UI框架称为 组件测试) 测试的单个widget。
- 集成测试:测试一个完整的应用程序或应用程序的很大一部分。
表格中总结了在不同类型测试之间进行选择的权衡:
纬度 | 单元测试 | widget测试 | 集成测试 |
---|---|---|---|
Confidence | Low | Higher | Highest |
维护成本 | Low | Higher | Highest |
依赖 | Few | More | Lots |
执行速度 | Quick | Slower | Slowest |
1.单元测试
单元测试主要是针对某个方法、类或者某一块逻辑进行逻辑校验,具体步骤如下:
1. 在pubsplc.yarm
中添加 flutter_test
的依赖:
dev_dependencies:
flutter_test:
sdk: flutter
2. 创建测试文件
项目创建的时候回生成一个默认的测试文件,可以直接使用,或者在test
目录下创建新的测试文件,这里直接创建: tool_test.dart
.
├flutter_app
├── lib
│ ├── XXXX_page.dart
├── test
│ ├── tools_test.dart
├── widget_test.dart
3. 编写测试类
校验手机号长度的一个简单方法:
static bool checkPhoneLength(String phone) {
if (phone == null || phone.isEmpty) {
return false;
}
return phone.length == 11;
}
4. 编写测试类
test(...)
方法里面有两个必需的参数,第一个参数表示这个单元测试的描述信息,第二个是一个 Function,用来编写测试内容的。
expect(...)
方法中也有两个必需的参数,第一个是需要验证的变量,第二个是与该变量匹配的值。
在tool_test.dart
中编写测试代码:
void main() {
///
/// 单一的测试
///
test('check phone length', () {
expect(CheckLength.checkPhoneLength('01234567891'), true);
expect(CheckLength.checkPhoneLength('0123456789'), false);
});
///
/// 多个测试一起 使用group
///
group('use group check', () {
test('check phone1', () {
expect(CheckLength.checkPhoneLength('01234567891'), true);
});
test('check phone2', () {
expect(CheckLength.checkPhoneLength('0123456789'), false);
});
});
}
4. 运行
点击左边侧运行测试内容,查看运行结果:
运行2.widget测试
和单元测试不同,widget测试可以验证widget组件创建、交互等操作。他使用的是WidgetTester
函数,在WidgetTester
函数中查找具体的widget:
testWidgets('widget test', (WidgetTester tester){
});
查找具体的widget通过顶层函数find
来操作,具体的函数有:
find.text('title'); // 通过 text 来定位 widget
find.byIcon(Icons.add); // 通过 Icon 来定位 widget
find.byWidget(myWidget); // 通过 widget 的引用来定位 widget
find.byKey(Key('value')); // 通过 key 来定位 widget
创建一个用来测试的widget test_page.dart
:
import 'package:flutter/material.dart';
class TestPage extends StatelessWidget {
final String title;
final String message;
const TestPage({Key key, @required this.title, @required this.message}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: Scaffold(
appBar: AppBar(
title: Text(title),
),
body: Center(
child: Text(message),
),
),
);
}
}
在test目录下创建widget_test.dart
文件:
import 'package:flutter_test/flutter_test.dart';
import 'package:flutterapp/test_page.dart';
void main() {
testWidgets('widget test', (WidgetTester tester) async {
// 加载 TestPage
await tester.pumpWidget(TestPage(
title: "T",
message: "M",
));
final titleFinder = find.text('T');
final messageFinder = find.text('M');
// 验证页面中是否含有上述的两个 Text
expect(titleFinder, findsOneWidget);
expect(messageFinder, findsOneWidget);
});
}
Matchers:
findsOneWidget //验证找到有且只有一个widget
findsNothing //验证没有可被查找的 widgets。
findsWidgets //验证一个或多个 widgets 被找到。
findsNWidgets //验证特定数量的 widgets 被找到。
关于测试中和widget进行交互的测试逻辑,官方的例子:
import 'package:flutter/material.dart';
class TodoList extends StatefulWidget {
TodoList({Key key}) : super(key: key);
@override
_TodoListState createState() => _TodoListState();
}
class _TodoListState extends State<TodoList> {
static const _appTitle = 'Todo List';
final todos = <String>[];
final controller = TextEditingController();
@override
Widget build(BuildContext context) {
return MaterialApp(
title: _appTitle,
home: Scaffold(
appBar: AppBar(
title: Text(_appTitle),
),
body: Column(
children: <Widget>[
TextField(
controller: controller,
),
Expanded(
child: ListView.builder(
itemCount: todos.length,
itemBuilder: (BuildContext context, int index) {
final todo = todos[index];
return Dismissible(
key: Key('$todo$index'),
onDismissed: (direction) => todos.removeAt(index),
child: ListTile(title: Text(todo)),
background: Container(color: Colors.red),
);
}),
),
],
),
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(() {
if (controller.text.isNotEmpty) {
todos.add(controller.text);
controller.clear();
}
});
},
child: Icon(Icons.add),
),
),
);
}
}
测试逻辑:
testWidgets('Add and remove a todo', (WidgetTester tester) async {
// Build the widget
await tester.pumpWidget(TodoList());
// 往输入框中输入 hi
await tester.enterText(find.byType(TextField), 'hi');
// 点击 button 来触发事件
await tester.tap(find.byType(FloatingActionButton));
// 让 widget 重绘
await tester.pump();
// 检测 text 是否添加到 List 中
expect(find.text('hi'), findsOneWidget);
// 测试滑动
await tester.drag(find.byType(Dismissible), Offset(500.0, 0.0));
// 页面会一直刷新,直到最后一帧绘制完成
await tester.pumpAndSettle();
// 验证页面中是否还有 hi 这个 item
expect(find.text('hi'), findsNothing);
});
3.集成测试
集成测试主要用到的是FlutterDriver
,它提供API去测试运行在真实设备和模拟器里面的Flutter应用。
- Flutter的Driver是:
- 一个命令行工具
flutter drive
- 一个包
package:flutter_driver
- 一个命令行工具
- 这两者做的操作是:
- 为集成测试创建指令化的应用程序
- 写一个测试
- 运行测试
1.添加依赖:
要使用flutter_driver
,您必须将以下块添加到您的pubspec.yaml
:
dev_dependencies:
flutter_driver:
sdk: flutter
2.添加测试文件
在项目根目录创建test_driver
目录和lib
目录同级,同时创建app.dart
和app_test.dart
文件:
├flutter_app
├── lib
│ ├── XXXX_page.dart
├── test
│ ├── tools_test.dart
├── widget_test.dart
├── test_driver
│ ├── app.dart
├── app_test.dart
为什么要创建两个文件,官方解释:
- 创建xx.dart文件:用于启动运行应用
- 创建xx_test.dart文件:Test脚本文件
- 集成测试中TestCase和应用运行在不同的进程中,所以需要test_driver目录里有两个文件分别用来执行应用和执行TestCase
app.dart:
import 'package:flutter_driver/driver_extension.dart';
import 'package:flutterapp/main.dart' as app;
void main() {
// 启用FlutterDriver扩展
enableFlutterDriverExtension();
// 启动执行应用
app.main();
}
解释:一个指令化的应用程序是一个Flutter应用程序,它启用了Flutter Driver 扩展。启用扩展请调用enableFlutterDriverExtension()。
app_test.dart:
在该文件中我们进行一个列表点击跳转,跳转后的页面中一个Key
为‘title’
的widget
的text
是否为‘Osechinen Lake Campground’
的操作:
//import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_driver/flutter_driver.dart';
import 'package:test/test.dart';
void main() {
group('Counter App', () {
// 通过key属性定位元素
final listTileWidget = find.byValueKey('detail');
FlutterDriver driver;
// 测试开始前链接FlutterDriver
setUpAll(() async {
driver = await FlutterDriver.connect();
});
// 测试结束后关闭FlutterDriver
tearDownAll(() async {
if (driver != null) driver.close();
});
// TestCase
test('increments the counter', () async {
//点击TitleList
await driver.tap(listTileWidget);
// 去第二个界面里面拿到具体的widget
final title = await driver.getText(find.byValueKey('Title'));
expect(title, 'Osechinen Lake Campground');
});
});
}
3.运行
连接设备,在项目路径终端运行命令:
flutter drive --target=test_driver/app.dart
得到结果:
运行结果
网友评论