Flutter自动测试探索

Posted 鲸鱼漫步

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Flutter自动测试探索相关的知识,希望对你有一定的参考价值。

        根据flutter官方文档的说明,flutter可以对我们的应用,进行自动化测试,保证我们应用的稳定性和功能的完整性,并且可以快速修复问题。

自动化测试可分为以下几类:

单元测试:测试单一的函数,方法或类

组件测试:测试单一的 widget

集成测试:测试一个完整的应用或者一个应用的大部分功能。

        我的整个探索,也是根据这上面3项项进行的。

官方文档地址:

测试 Flutter 应用 - Flutter 中文文档 - Flutter 中文开发者网站 - Flutter学习不同类型的测试以及如何编写它们。https://flutter.cn/docs/testing

一.单元测试

        测试单一的函数,方法或类。单元测试的目标是验证逻辑单元在各种条件下的正确性。因为,在很多对日项目,都在追求单元测试覆盖率的前提下,推荐以一个物理文件为单位,编写一组测试。整个过程如下:

        step1.新建一个Flutter 项目 fluttertest并编写一个逻辑处理类lib/counter.dart

class Counter 
  int value = 0;

  void increment() => value++;

  void decrement() => value--;

        step2.项目追加test依赖

 flutter pub add test

         step3.创建一个单元测试类,在test目录中 counter_test.dart

import 'package:counter_app/counter.dart';
import 'package:test/test.dart';

void main() 
  group('Counter', () 
    test('value should start at 0', () 
      expect(Counter().value, 0);
    );

    test('value should be incremented', () 
      final counter = Counter();

      counter.increment();

      expect(counter.value, 1);
    );

    test('value should be decremented', () 
      final counter = Counter();

      counter.decrement();

      expect(counter.value, -1);
    );
  );

        这样单元测试就编写好了,单元测试主要测试的是逻辑处理单元,用到了test作为一个测试单元,在其中进行相关的测试逻辑编写,并使用expect进行期望判定,同时,可以通过group对test进行分类。

        step4.通过命令,可以运行单元测试,并获取相应的测试报告。

flutter test --coverage 

        step5.执行上述的测试后,我们可以在项目根目录中找到coverage文件夹,在文件夹中已经为我们生成好了lcov.info文件,该文件中计入了测试的覆盖率信息,我们可以通过vscode 上的插件对他进行简单的查看。VSCode LCOV - Visual Studio Marketplace

        当然,我们还需要对这个文件进行转换,生成相应的测试报告。这个转换过程在windows下,我们需要搭建Linux环境,并通过相应的指令进行。环境搭建需要依次安装下面的内容。

MinGW-W64->msys2->lcov 相关的软件我都是在SourceForge上找到并安装。搜关键字即可。
Compare, Download & Develop Open Source & Business Software - SourceForgeSourceForge provides free & fast open source software downloads and development, and business software reviews and comparisons featuring the largest open source and business software directory.https://sourceforge.net/安装的简单说明:不会的不明白的可以自己百度,这个没什么难度。不浪费篇幅,贴图了。

  • MinGW-W64 和msys2都只下一步就可以了。
  • lcov 是解压文件,需要解压,并把lcov、gendesc、genhtml、geninfo、genpng复制到mingw64的bin目录下。然后在吧整个bin目录加到系统的环境变量中path中。
  • 找到msys2的shell入口cmd文件C:\\msys64\\msys2_shell.cmd,修改该文件。检索set MSYS2_PATH_TYPE=inherit 把该行的rem去掉,让其在cmd运行时执行,以便把我们的windows系统环境变量,加入到msys2中。

        经过上面的安装,我们再msys2的shell环境中,进入到我们的项目目录,如果你的项目在e盘,你需要通过cd /e/ 来进入。

当你进入到的项目根目录后,你需要执行下面的指令,来完成转换

genhtml coverage/lcov.info -o coverage/html

Administrator@BAC1901170-PC MSYS /e/test/fluttertest
$ genhtml coverage/lcov.info -o coverage/html
Reading data file coverage/lcov.info
Resolved relative source file path "lib\\counter.dart" with CWD to "/e/test/fluttertest/lib/counter.dart".
Found 2 entries.
Found common filename prefix "/e/test/fluttertest"
Writing .css and .png files.
Generating output.
Processing file lib/main.dart
Processing file lib/counter.dart
Writing directory view page.
Overall coverage rate:
  lines......: 92.3% (24 of 26 lines)
  functions..: no data found

然后再到自己的根目录下,就可以看到html文件夹,双击html/index.html 就可以看到报告了。

总结:

上述内容容易理解,记住一下二点即可:

1.flutter单元测试主要用到 test expect group 三个函数

2.理解到单元测试的主要目标是,测试逻辑的正确性

另外:覆盖率是度量测试完整性的一个手段,是测试有效性的一个度量

测试覆盖是对测试完全程度的评测。覆盖率_百度百科

2.组件测试

        组件测试,就是widget测试,对widget类的测试,这部分的测试弥补了单元测试只能测试业务逻辑而不能测试画面组件的缺憾。从而提高我们整个测试的覆盖率。

        当你新建一个flutter项目时,创建模板就已经将组件测试包含到你的项目中了,你无需再添加flutter_test依赖,就可以使用他了。

dev_dependencies:
  flutter_test:
    sdk: flutter

        当然你也将在test文件夹中,找到widget_test文件,在这个文件里已经编写好了一个典型的widget测试。我们一起学习一下他的构造。

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

import 'package:fluttertest/main.dart';

void main() 
  testWidgets('Counter increments smoke test', (WidgetTester tester) async 
    // Build our app and trigger a frame.
    await tester.pumpWidget(const MyApp());

    // Verify that our counter starts at 0.
    expect(find.text('0'), findsOneWidget);
    expect(find.text('1'), findsNothing);

    // Tap the '+' icon and trigger a frame.
    await tester.tap(find.byIcon(Icons.add));
    await tester.pump();

    // Verify that our counter has incremented.
    expect(find.text('0'), findsNothing);
    expect(find.text('1'), findsOneWidget);
  );

testWidgets测试方法,这个方法中定义了我们整个widget测试的内容。

WidgetTester提供的 pumpWidget方法会建立并渲染我们提供的 widget。在调用pumpwidget后,我们还可以调用pump等函数用于重新构建widget,这个对StatefulWidget 或者动画会非常有用。

在一个expect函数中,我们可以通过expct(Finder,Matcher)来验证,控件的内容期望。

案例是通过find.text这个Finder,来查找指定文本内容的组件,通过findsOneWidget 这个Matcher来进行判断

常用的Matchers:

findsOneWidget                  验证widget 只在屏幕中出现一次

findsNothing                        验证没有可被查找的 widgets。

findsWidgets                       验证一个或多个 widgets 被找到。

findsNWidgets                     验证特定数量的 widgets 被找到。

matchesGoldenFile             验证渲染的 widget 是否与特定的图像匹配(「目标文件」测试)。

常用的finder构造函数:

find.text 它会创建一个 Finder 来寻找显示特定文本 String 的 widget

find.byKey 通过已经提供给 widget 的 Key 来查找 widget

如果上述示例不适用于一些特殊情况,请到 CommonFinders 文档 中查看更多用法

WidgetTester 提供了文本输入、点击、拖动的相关方法:

在很多情况下,用户交互会更新应用状态。在测试环境中,Flutter 在状态发生改变的时候并不会自动重建 widget。为了保证模拟用户交互实现后,widget 树能重建,一定要调用 WidgetTester 提供的 pump() 或 pumpAndSettle()

3.集成测试

        Unit tests 和 Widget tests 并不能够测试单独的模块形成的整体或者获取真实设备上应用运行状态。这些任务需要集成测试 (integration tests) 来处理。

        他能够达到的效果是在真机上运行测试脚本,进行自动化测试。正体编写与单元测试和组件测试类似。具体步骤如下:

        step1.引入依赖

dev_dependencies:
  integration_test:
    sdk: flutter
  flutter_test:
    sdk: flutter

        step2.项目根目录下创建integration_test 文件夹,编写一个app_test.dart的文件,内容如下

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

import 'package:integration_test/integration_test.dart';


void main() 
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();

  group('end-to-end test', () 
    testWidgets('Main Test', (WidgetTester tester) async 
      //进入主程序
      await tester.pumpWidget(MyApp());
      //等待加载一会
      await Future.delayed(const Duration(seconds: 3));
      //找到对应的入力框
      Finder userFinder = find.byType(TextFormField).at(0);
      Finder passwordFinder = find.byType(TextFormField).at(1);
      expect(userFinder, findsOneWidget);
      expect(passwordFinder, findsOneWidget);
      //填写用户名密码
      await tester.enterText(userFinder, 'xxxx');
      await tester.enterText(passwordFinder, 'xxxx');
      await tester.pumpAndSettle();
      
      //等待3秒后执行点击登录按钮操作
      await Future.delayed(const Duration(seconds: 3)).then((value) async 
        Finder loginFinder = find.byType(LoginButton);
        expect(loginFinder, findsOneWidget);
        await tester.tap(loginFinder);
        //需要等待动画加载画面迁移(此处不能省略)
        await tester.pumpAndSettle();
        await Future.delayed(const Duration(seconds: 10));
      );


    );
  );

注意:await tester.pumpAndSettle();的调用,在用动画的画面,迁移画面时必须调用,否则看不到相应动画的测试与调用。

        step3.启动设备,运行测试 

flutter test integration_test --coverage

        step4.查看报告(参2.组件测试)


其他测试相关内容,有待探索。。。

《TODO》处理滚动

处理滚动 - Flutter 中文文档 - Flutter 中文开发者网站 - Flutter如何在 widget 测试中处理滚动。https://flutter.cn/docs/cookbook/testing/widget/scrolling

《TODO》模拟(Mocks)允许我们仿造一个线上服务或数据库使用 Mockito 模拟依赖关系 - Flutter 中文文档 - Flutter 中文开发者网站 - Flutter使用 Mockito package 在测试中模拟服务端行为。https://flutter.cn/docs/cookbook/testing/unit/mocking

《TODO》为 plugin 编写单元测试如何测试 Flutter Pluginhttps://flutter.cn/docs/development/packages-and-plugins/plugins-in-tests

Flutter UI自动化测试技术方案选型与探索

作者:闲鱼技术——小匠

Flutter页面无法直接使用Native测试工具定位元素,给自动化测试带来很多不便。虽然Google官方推出了Flutter driver 和 Integration test,但是在实际使用中存在以下问题:

•不适用于混合栈APP,虽然appium中有相关的driver,但是无法切换环境。•元素定位能力相对薄弱。•依赖于VMService,需要构建Profile或Debug包。

基于以上因素,我们并没有直接使用Google官方推出的工具,而是选择基于Native测试工具去扩展Flutter页面的测试能力。本文对Flutter driver 和Integration test的原理和实现进行了分析,同时简单介绍闲鱼在UI自动化测试的尝试方案。

一、Flutter driver

最早接触flutter自动化测试时,先尝试使用appium框架去驱动APP,当我们使用inspect功能去dump页面元素时发现很多元素会被合并成一个区域块,然后点击的时候只能通过xpath定位,想定位到某些具体的元素会比较困难,并且xpath其实是容易改变的,代码可维护性能力差。 因为上述原因,我们开始调研Flutter官方提供的测试工具——flutter driver。一开始使用该框架的时候发现它只能适用于纯Flutter应用,对于混合栈应用并不适应,但是它底层提供的元素定位能力或许对我们有用,于是我们对它的源码进行了剖析,该框架的原理图1如下所示。flutter

图1 flutter driver原理图 整个框架的流程交互比较简单,测试脚本在运行时,首先利用FlutterDriver.connect()来连接VMService获取相关的isolate,之后通过websocket来传输操作过程以及数据获取。其中测试脚本侧的所有操作都是被序列化为json字符串通过websocket传递给ioslate来转换为命令在APP侧执行,例如我们想要获取某个组件的文本内容,其最终生成的json结构体如下:

{
    "jsonrpc":"2.0",
    "id":5,
    "method":"ext.flutter.driver",
    "params":{
        "finderType":"ByValueKey",
        "keyValueString":"counter",
        "keyValueType":"String",
        "command":"get_text",
        "isolateId":"isolates/4374098363448227"
    }
}

了解上述原理后,就可以通过构造协议格式,在任何语言、测试框架下都能够去驱动flutter测试,所以我们对这个协议进行了封装,使用Python进行驱动,这样可以在使用uiautomator2和facebook-wda的基础上来测试flutter页面,以满足flutter混合栈应用的测试需求。最终的实现代码demo如下。

from flutter_driver.finder import FlutterFinder
from flutter_driver.flutter_driver import FlutterDriver
import uiautomator2 as u2

if __name__ == "__main__":
    d = u2.connect()
    driver = FlutterDriver(d)
    if pageFlutter is True:  # 如果是flutter,则使用flutter driver进行驱动
        driver.connect("com.it592.flutter_app")
        finder = FlutterFinder.by_value_key("input")
        driver.tap(finder)
        time.sleep(1)
        print(driver.getText(FlutterFinder.by_value_key("counter")))
    else:
        d(text="increase").click()

我们尝试使用该套框架,发现其实flutter driver底层提供的能力相对比较薄弱,并不能完全满足我们的需求,主要问题如下:

•不能批量操作元素,一旦finder定位到的元素超过1个时,就会抛出异常。•很多时候开发同学不写key,元素定位也没那么方便。•因为flutter没有inspect工具dump元素,所以只能利用结合源码去写脚本,代码维护成本比较高。•官方已经放弃维护该项目,所以后续估计也不会有新功能支持。

二、integration_test

前面提到,flutter官方放弃维护Flutter driver,并推出新的测试框架integration_test,那么这个框架会不会对混合栈应用予以支持呢,事实上试用了之后发现事情并没有我们想的那么美妙。在官方文档里有这么一句话“该软件包可在设备和模拟器上对Flutter代码进行自驱动测试”。 integration_test底层的元素操作和定位还是基于flutter_test去驱动的,其优势主要如下:

•测试脚本可以使用各种Flutter的API。•打包ipa、apk后就能在 Firebase Test Lab等设备群上运行测试,不需要额外驱动。•integration_test的每个页面之间测试无关联,可以实现单个页面级别的测试。

但是由于底层元素定位和Flutter driver的是一致的,所以Flutter driver存在的问题依旧存在,同时还存在其他局限问题:

•测试脚本打包到APP中,每次修改脚本都需要重新打包。•对端到端测试不够友好,需要额外函数来等待数据加载完毕。•不适合全链路级别的页面测试。•可扩展性弱

基于以上问题,不满足我们的使用需求,所以我们只是做了简单预研,并没有深入了解和应用。​

三、闲鱼UI自动化测试方案

学习Flutter官方推出的相关测试框架之后,我们开始思考闲鱼UI自动化到底要怎么走?是站在官方的肩膀上去造轮子还是复用现有的原生自动化测试能力去扩展Flutter测试能力。在综合考虑投入成本以及测试脚本的维护难度后,我们选择使用图像处理技术来扩充原生自动化框架对Flutter页面的测试能力支持,整个测试方案架构如图2所示。UI自动化框架

图2 闲鱼UI自动化测试方案架构 Flutter的元素不是完全不能被uiautomator2和facebook-wda识别,所以编写测试脚本时只需要处理不能被识别的元素即可。对于有name、label以及xpath不易改变的元素定位,我们优先使用原生定位能力进行定位操作,其他元素则直接使用图像处理技术进行定位操作。 在处理无法使用原生能力定位的元素时,我们优先使用ocr文字匹配来进行定位,准确率较高,不容易受分辨率的影响,对于纯图片则通过图片查找的方式进行定位。对于一些常见的元素控件例如商品卡片、价格、icon、头像等,我们构建一个训练集,使用图像分类来判断元素的类型,从而实现常用控件的定位。 UI自动化面临最大的问题就是——随着版本的迭代,测试脚本也需要进行不断迭代。所以在方案选型和脚本编写过程中需要考虑到脚本的健壮性以及可维护性。我们在实际脚本开发中将页面元素封装到单独的类中,并与测试逻辑分离,从而保证后期元素迭代时只需要修改对应的页面元素即可,减少维护成本。1.png

图3 脚本分层结构 闲鱼性能自动化测试的相关UI操作已经使用该方案,在脚本编写时,并不需要区分当前页面是什么类型。我们的脚本已经稳定运行500+次,成功率超过98%。

四、总结

图4 方案对比 从图4可以看出,无论是flutter driver还是integration test对混合栈的支持不够成熟,但是flutter driver可以进行一些扩展,对于纯Flutter应用而言,采用该方案能够基本满足测试需求,而integration test相对没有那么成熟,对于混合栈应用的测试,可能还是需要考虑混合栈的场景切换成本,使用一些ocr技术去做一些扩充可能成本更低,收益更大。

最后

小编在网上收集了一些 Android 开发相关的学习文档、面试题、Android 核心笔记等等文档,希望能帮助到大家学习提升,如有需要参考的可以直接去我 codechina地址:https://codechina.csdn.net/u012165769/Android-T3 访问查阅。

以上是关于Flutter自动测试探索的主要内容,如果未能解决你的问题,请参考以下文章

Flutter UI自动化测试技术方案选型与探索

Flutter UI自动化测试技术方案选型与探索建议收藏

做了2个多月的设计和编码,我梳理了Flutter动态化的方案对比及最佳实现

京东APP中Flutter探索及优化

探索Flutter & 腾讯移动通讯 TPNS

携手Flutter和Kotlin探索Android11