Flutter 自动化测试

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.dartapp_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’widgettext是否为‘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

得到结果:


运行结果
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,509评论 6 504
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,806评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,875评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,441评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,488评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,365评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,190评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,062评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,500评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,706评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,834评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,559评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,167评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,779评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,912评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,958评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,779评论 2 354