Flutter自动化集成测试

564 阅读7分钟

(前言:好久没有更新文章了,之前加班比较多,趁今天周末把之前总结的在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:

  1. 查找类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');
  1. 交互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');
  1. 其他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,我发现相同的测试代码有时候可以正常执行,有时候报错找不到对应的元素,猜测是进入一个新页面,页面还未渲染完毕就获取元素,所以才报错找不到元素。

三、持续更新中.......