美文网首页
Flutter 自动化测试

Flutter 自动化测试

作者: 陈贤森 | 来源:发表于2020-06-29 18:22 被阅读0次

    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
    

    得到结果:


    运行结果

    相关文章

      网友评论

          本文标题:Flutter 自动化测试

          本文链接:https://www.haomeiwen.com/subject/vzdafktx.html