(前言:好久没有更新文章了,之前加班比较多,趁今天周末把之前总结的在Flutter中自动化集成测试的一个文档整理一下)
一、背景
- 背景:1、网上关于Flutter集成测试的文章比较少,有的都是比较简单的API调用,在实际操作中遇到的很多问题网上都没有正确的解决方案;2、在项目升级过程中,修改了某些地方,为了能快速回归测试,减少bug个数,提升提测质量,编写本文档。
- 技术应用场景:适用于Flutter自动化集成测试案例的编写(文中附有具体demo案例,测试人员和开发人员都可参考编写集成测试案例)。
二、操作步骤
2.1 准备环境
准备工作
-
开发工具Android Studio,在pubspec.yaml 添加测试依赖
flutter_driver
dev_dependencies:
flutter_test:
sdk: flutter
#添加集成测试相关依赖
flutter_driver:
sdk: flutter
test: any
常用API:
- 查找类API
- find.text('xxx') 通过文本查找Widget
- find.byType('xxx') 通过Widget 类名查找Widget
- byValueKey(key) 通过Widget绑定的key 查找Widget
- byTooltip('xxx') 通过 tooltip 查找Widget
- bySemanticsLabel(T) 通过语义化标签查找Widget
- pageBack 在Scaffold 页面中找到返回按钮
- descendant 查找子级元素,其他查找方式不能满足我们需求时可以使用,有2个必传参数(of,matching),可以这样理解:先通过of 查找到父级元素finder1,再在finder1 里面向下级查找,一直查找到matching 为止。例如:我想要查找下图的Image元素,但是页面中Image有很多,我们可以先找到他的祖先元素NLUserAgreement,再在NLUserAgreement查找类型为Image的元素:
final img = find.descendant(of: find.byType('NLUserAgreement'), matching: find.byType('Image'));
- ancestor 查找符合条件的祖先元素,用法与descendant类似
开发中最常用的是前3个方法
示例:
var btn = find.text('同意');
var btn = find.byType('NLUserInfoWidget');
var btn = find.byValueKey('key');
- 交互API:
- tap点击Widget
- waitForTappable 等待元素变为可点击状态
- enterText 输入文本
示例:
final loginBtn = find.text('登录');
await driver!.waitForTappable(loginBtn);
await driver!.tap(loginBtn);
var pwdInput = find.byType('PasswordInputWidget');
await driver!.tap(pwdInput);
await driver!.enterText('Aa111111');
- 其他API
其他API可参考官方文档:api.flutter.dev/flutter/flu… 和 GitHub文档:github.com/flutter/flu…
测试步骤
-
在项目根目录添加测试文件:test_driver,在test_driver 目录下新建两个文件:xxx.dart 和 xxx_test.dart,我这里为了按模块区分(此案例是以手机号密码登录模块为案例),又新建了一个login文件夹,然后添加的文件名称分别是login_by_phone_pwd.dart 和 login_by_phone_pwd_test.dart (login_by_phone_pwd.dart 文件会自动去识别login_by_phone_pwd_test.dart 文件,所以命名一定要按此格式。) 在app.dart 中调用enableFlutterDriverExtension 方法,然后调用main 函数,enableFlutterDriverExtension方法一定要在main函数之前调用。
app.dart文件:
import 'package:flutter_driver/driver_extension.dart';
import 'package:nolovr_assistant/main.dart' as app;
void main () {
enableFlutterDriverExtension();
app.main();
}
app_test.dart文件:
import 'dart:io';
import 'package:flutter_driver/flutter_driver.dart';
import 'package:test/test.dart';
void main () {
loginText(); // loginText 是编写具体的测试案例的函数
}
//在这里编写具体的测试案例
//本例以手机首次安装APP开始一直到登录成功 测试登录为案例说明API的使用 以及遇到的问题
//本例测试流程:APP首次安装->弹出用户隐私协议->点击‘同意’按钮->点击‘我的’tab按钮,进入‘我的’页面 ->
//点击‘头像’进去切换环境页面->点击‘开发环境’,切换到开发环境 ->点击Appbar上返回箭头,返回上个页面 ->
//点击‘立即登录’ 进入到 手机号+验证码 登录页面 -> 点击‘账号密码登录’进入 账号+密码登录页面->
//输入手机号 输入密码,点击CheckBox 同意服务协议 ->点击‘登录’开始登录 ->登录成功 展示‘登录成功’信息 ->
//获取到‘登录成功’ 文字,即代表 账号+密码 登录流程测试OK
loginText(){
print("---------开始测试---------");
group('登录模块测试集合', () {
FlutterDriver? driver;
//测试开始前链接
setUpAll(() async {
driver = await FlutterDriver.connect();
});
//测试结束后关闭链接
tearDownAll(() async {
if (driver != null) {
driver!.close();
}
});
test('check driver health', () async{
// 检查 driver 的状态
Health health = await driver!.checkHealth();
print('------driver health state---${health.status}');
});
test('登录模块测试-手机号密码登录', () async {
try {
final _driver = driver!;//避免每次使用driver变量时都做空安全判断
await _driver.runUnsynchronized(() async {
//等待页面加载完成,出现 '同意' 用户隐私协议的按钮
await _driver.waitFor(find.text('同意'));
var btn = find.text('同意');
await _driver.tap(btn);
var text = await _driver.getText(btn);
//期望获取的文字是‘同意’,如果不是则测试流程结束
expect(text, '同意');
//获取 '我的' tabbar
final tabBarMy = find.text('我的');
//点击 '我的' 进入我的页面
await _driver.tap(tabBarMy);
//点击头像 切换测试环境
await _driver.tap(find.byType('NLUserInfoWidget'));
print('点击头像,进入切换环境页面---------');
await _driver.tap( find.text('开发环境'));
await _driver.waitFor( find.text('当前环境已生效'));
print('切换环境成功-------');
sleep(Duration(seconds: 1));
//找到返回按钮 返回上个页面
await _driver.tap( find.byType('MaterialButton'));
print('----返回到我的页面---');
sleep(Duration(seconds: 2));
//获取 立即登录 按钮
final loginBtn = find.text('立即登录');
await _driver.tap(loginBtn);
print('进入 手机号+验证码 登录页面----------');
final accountAndPwdBtn = find.text('账号密码登录');
await _driver.tap(accountAndPwdBtn);
print('进入 手机号+密码 登录页面');
//找到手机号输入框
var phoneInput = find.byType('ISOCodePhoneNumberInputWidget');
await _driver.tap(phoneInput);
//测试手机号不正确情况
await _driver.enterText('11111111111');
// await _driver.enterText('15122657281');
print('输入手机号----------');
var pwdInput = find.byType('PasswordInputWidget');
await _driver.tap(pwdInput);
await _driver.enterText('Aa111111');
print('输入密码----------');
// final checkBox = find.text('登录即同意 ');
final checkBox = find.byType('NLInkwell');
await _driver.tap(checkBox);
print('点击 登录即同意 radio----------');
//点击登录按钮
print('------开始获取登录按钮');
final loginBtn2 = find.text('登录');
await _driver.waitForTappable(loginBtn2);
await _driver.tap(loginBtn2);
await _driver.waitFor(find.text('请输入正确的手机号码'));
var errorMsg = await _driver.getText(find.text('请输入正确的手机号码'));
expect(errorMsg, '请输入正确的手机号码');
sleep(Duration(seconds: 1));
//然后输入正确手机号
await _driver.tap(phoneInput);
_driver.enterText('15122657281');
sleep(Duration(seconds: 1));
await _driver.waitForTappable(loginBtn2);
await _driver.tap(loginBtn2);
print('登录成功!----------');
//登录成功,进入首页
await _driver.waitFor(find.text('登录成功'));
final successText = await _driver.getText(find.text('登录成功'));
expect(successText, '登录成功');
sleep(Duration(seconds: 2));
print('---------账号密码登录测试完毕----------');
});
} catch (e) {
print('测试出错-------$e');
}
}, timeout: Timeout( Duration(seconds: 30)));
});
}
案例编写完毕,在命令行中切换到项目根目录,执行命令:
flutter driver test_driver/login/login_by_phone_pwd.dart -d M2007J3SC
命令中 -d M2007J3SC 是设备名称,换成自己的设备名称即可,不加-d 参数也没关系,执行后会提示你选择一个测试设备。
遇到的问题总结:
- 问题1:执行命令报错:Flutter Driver extension is taking a long time to become available....
VMServiceFlutterDriver: Flutter Driver extension is taking a long time to become available.
Ensure your test app (often "lib/main.dart") imports "package:flutter_driver/driver_extension.dart"
and calls enableFlutterDriverExtension() as the first call in main().
解决方案:将要执行的所有方法都放在driver. runUnsynchronized 里面执行,不要问什么,百度了n长时间也没找到解决方案,就是尝试n+1次都失败后发现这样可以正确执行测试命令😂。
-
问题2:VMServiceFlutterDriver: xxx message is taking a long time to complete...
- xxx可能是tap、waitFor或其他命令,出现这个错误很大的原因是没有找到正确的Widget,例如使用find.text('测试字符串') ‘测试字符串’ 文本不存在或者 ‘测试字符串’是一个整体,但是执行的是find.text('测试'),也会出现这种情况
- 还有一种情况是使用find.byType('WidgetType'),出现这种错误,很可能是使用了静态构造方法的Widget,因为静态方法在执行是没有被实例化,所以在Widget树种是不存在的,本项目中 NLImage.image_size 用这种方法就会出现此种错误,要看一个Widget是否在Widget树种是否存在,可以结束Flutter的调试工具:Flutter Inspactor
在这里我们可以发现代码中的 NLImage 类在Widget树种没有显示,还有我们用Text.rich 组件包装的TextSpan ,被渲染成了一行文档,而且前后是有空格的,如果不带空格的文本去获取Widget是获取不到的(我被这个问题困扰了很久才解决😂)。
- 问题3
执行命令后,在命令行出现类似于下面的报错
../../../../../development/flutter_2.8.1/packages/flutter/lib/src/material/animated_icons.dart:9:8: Error: Not found: 'dart:ui'
import 'dart:ui' as ui show Paint, Path, Canvas;
^
../../../../../development/flutter_2.8.1/packages/flutter/lib/src/material/animated_icons.dart:10:8: Error: Not found: 'dart:ui'
import 'dart:ui' show lerpDouble;
^
../../../../../development/flutter_2.8.1/packages/flutter/lib/src/material/app.dart:5:8: Error: Not found: 'dart:ui'
import 'dart:ui' as ui;
^
../../../../../development/flutter_2.8.1/packages/flutter/lib/src/material/app_bar_theme.dart:5:8: Error: Not found: 'dart:ui'
import 'dart:ui' show lerpDouble;
^
../../../../../development/flutter_2.8.1/packages/flutter/lib/src/material/arc.dart:6:8: Error: Not found: 'dart:ui'
import 'dart:ui' show lerpDouble;
^
../../../../../development/flutter_2.8.1/packages/flutter/lib/src/material/banner_theme.dart:5:8: Error: Not found: 'dart:ui'
import 'dart:ui' show lerpDouble;
^
../../../../../development/flutter_2.8.1/packages/flutter/lib/src/material/bottom_app_bar_theme.dart:5:8: Error: Not found: 'dart:ui'
import 'dart:ui' show lerpDouble;
^
../../../../../development/flutter_2.8.1/packages/flutter/lib/src/material/bottom_navigation_bar_theme.dart:5:8: Error: Not found: 'dart:ui'
import 'dart:ui' show lerpDouble;
^
../../../../../development/flutter_2.8.1/packages/flutter/lib/src/material/bottom_sheet.dart:5:8: Error: Not found: 'dart:ui'
import 'dart:ui' show lerpDouble;
^
../../../../../development/flutter_2.8.1/packages/flutter/lib/src/material/bottom_sheet_theme.dart:5:8: Error: Not found: 'dart:ui'
import 'dart:ui' show lerpDouble;
^
这时候检查你的app_test.dart 头文件是不是引入了UI相关的类库如:
import 'package:flutter/material.dart';//引入了这种UI的类库 就会报上面的错误
问题4:官方提供的查找Widget API在某个页面中获取不到想要获取的Widget,如:页面中根据find.test('xxx') 获取不到想要的Widget,也没有可用的key供我们使用find.byValueKey('key'),根据find.byType('type') 获取的Widget有多个会报错,查阅了官网后没有找到合适的方式获取想要的Widget,这时候可以考虑修改driver源码,当使用 find.byType('type') 获取多个Widget时,选取其中一个,可以考虑修改下面这个地方的源代码:
在这里,当获取多个Widget时,我是取了最后一个element来使用,后面考虑copy一份这部分的代码出来封装一个插件,使之能传入一些参数控制选取的元素。
问题5:有时候进入一个页面中获取不到对应的元素,偶发现象。报错代码:
DriverError: Error in Flutter application: Uncaught extension error while executing tap: Bad state: No element
进入一个新页面最好用sleep()方法执行一个延时操作再获取想要的Widget,我发现相同的测试代码有时候可以正常执行,有时候报错找不到对应的元素,猜测是进入一个新页面,页面还未渲染完毕就获取元素,所以才报错找不到元素。