Flutter 自动化测试

175 阅读5分钟

App中的功能越来越多越复杂的时候,一些基本的功能的测试就可以交给Flutter提供的自动化测试来完成这些繁琐的工作。

Flutter中提供的三种测试:

  • 单元测试:测试单一功能、方法或类。
  • Widget 测试:(在其它UI框架称为 组件测试) 测试的单个widget。
  • 集成测试:测试一个完整的应用程序或应用程序的很大一部分。

表格中总结了在不同类型测试之间进行选择的权衡:

纬度单元测试widget测试集成测试
ConfidenceLowHigherHighest
维护成本LowHigherHighest
依赖FewMoreLots
执行速度QuickSlowerSlowest

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. 运行

点击左边侧运行测试内容,查看运行结果:

23926378-f3abfd3699b3214c.webp

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

得到结果:

23926378-ec462a8c537a9a4d.webp

这样一个简单的自动化测试就已经完成了,后续我们在完善更加复杂的自动化测试内容。