Flutter 学习之路 - 测试(单元测试,Widget 测试,集成测试)

flutter有问必答群号: 1051522925

关于 Flutter 的测试
一般来说,经过良好测试的应用应该有很多 unit tests 和 widget test,通过代码覆盖率进行跟踪,以及需要足够的集成测试来涵盖所有重要的使用场景。下面的表格,总结了在不同类型测试的特点,方便在选择的时候进行权衡:


测试.png

单元测试

参考文章(主要就是按这个学习翻译的,英文 ok 可以直接看官网):link

测试单一功能、方法或类。例如,被测单元的外部依赖性通常被模拟出来,如package:mockito。 单元测试通常不会读取/写入磁盘、渲染到屏幕,也不会从运行测试的进程外部接收用户操作。单元测试的目标是在各种条件下验证逻辑单元的正确性。

第一步:添加 test 或者 flutter_test 依赖

pubspec.yaml 里添加如下方法(嫌麻烦可以把冒号之后写 any)

dev_dependencies:
  test: <latest_version>

加上以后记得按一下 Packages get

第二步:创建测试文件

目录结构如下:(测试文件写在 test 文件里面)

flutter_road_test/
  lib/
    counter.dart
  test/
    counter_test.dart

第三步:创建要测试的类

创建一个要被测试的单元,这个单元可以是一个方法或一个类,下面在 lib/counter.dart 文件中创建 Counter 类:

class Counter {
  int value = 0;

  void increment() => value++;

  void decrement() => value--;
}

第四步:为这个类写 test

testexpect 都来自 test 这个包:

// Import the test package and Counter class
import 'package:test/test.dart';
import 'package:counter_app/counter.dart';

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

    counter.increment();

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

第五步:如果有多个测试,可以用 group

group 里面包含了三个测试(初始状态测试,increment 方法测试,counter.decrement 方法测试)

import 'package:test/test.dart';
import 'package:counter_app/counter.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);
    });
  });
}

第六步:运行测试

在命令行下运行:

flutter test test/counter_test.dart

结果:

image

其他运行方式可以在这里看:link

Widget 测试

参考文章(主要就是按这个学习翻译的,英文 ok 可以直接看官网):link

第一步:添加 flutter_test 包

为什么要用这个包,不用前面的 test 包呢,因为 flutter_test 包有下面这些功能:

  • 等等再补充
dev_dependencies:
  flutter_test:
    sdk: flutter

第二步:创建一个要测试的 Widget
class MyWidget extends StatelessWidget {
  final String title;
  final String message;

  const MyWidget({
    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),
        ),
      ),
    );
  }
}

第三步:写测试代码

有了要测试的 Widget 以后,可以开始写测试了:

  1. 创建一个 testWidgets 方法
  2. tester.pumpWidget 来创建一个 MyWidget
  3. finder 来在 Widget tree 中找到 title 和 message 的 Text Widgets
  4. expectfindsOneWidget 来测试这个 Widget 是不是只出现了一次
void main() {
  // with Widgets in the test environment.
  testWidgets('MyWidget has a title and message', (WidgetTester tester) async {
    // Create the Widget tell the tester to build it
    await tester.pumpWidget(MyWidget(title: 'T', message: 'M'));

    // Create our Finders
    final titleFinder = find.text('T');
    final messageFinder = find.text('M');

    // Use the `findsOneWidget` matcher provided by flutter_test to verify our
    // Text Widgets appear exactly once in the Widget tree
    expect(titleFinder, findsOneWidget);
    expect(messageFinder, findsOneWidget);
  });
}

第四步:运行测试

发现在 Android Studio 里邮件代码文件点运行就能运行了:

image
补充

[1] 第三步中的 WidgetTester 除了 pumpWidget 还提供了其他的方法,在使用 StatefulWidget 或者 animations 的时候可以用到:

[2] 第三步中的 findsOneWidget 是一个 Matcher,还有一些其他的 Matcher 可以用:

findsOneWidget 只有一个对应的 Widget
findsNothing 没有找到对应的 Widget
findsWidgets 找到一个或一个以上对应的 Widget
findsNWidgets 找到 N 个 Widget

最后那个查了一下 API, 是这么用的:
expect(find.text('Save'), findsNWidgets(2));

集成测试

参考文章(主要就是按这个学习翻译的,英文 ok 可以直接看官网):link

单元测试 和 Widget 测试可以用于测试单独的 class, function, 和 Widget。当要测试各部分一起运行或者测试一个 application 在真实设备上运行的表现的时候就要用到集成测试

第一步:创建要测试的 App

首先,要创建一个被测试的 App, 这个应该功能就是按悬浮按钮 +1,就是初始给的那个 demo, 不一样的在于这里我们给 TextFloatingActionButton 添加了 ValueKey 以便在测试时识别这些特点的 Widgets。

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Counter App',
      home: MyHomePage(title: 'Counter App Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              // Provide a Key to this specific Text Widget. This allows us
              // to identify this specific Widget from inside our test suite and
              // read the text.
              key: Key('counter'),
              style: Theme.of(context).textTheme.display1,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        // Provide a Key to this the button. This allows us to find this
        // specific button and tap it inside the test suite.
        key: Key('increment'),
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

第二步:添加 flutter_driver 依赖

在集成测试中要用到 flutter_driver,在 pubspec.yaml 中加入它。

dev_dependencies:
  flutter_driver:
    sdk: flutter
  test: any

同时,也添加了 test ,因为也要用到这里面的方法和断言。

第三步:写 test 代码
  1. 创建文件夹 test_driver(和 lib 文件同级)
  2. 在文件夹下创建两个文件(命名可以随意),一个是创建指令化的 Flutter 应用程序,使我们能 "运行" 这个app,并记录运行的 performance (app.dart),另一个用于写测试来判断app 是不是按预期运行(app_test.dart),目录如下:
flutter_road_test/
  lib/
    main.dart
  test_driver/
    app.dart
    app_test.dart

第四步:创建指令化的 Flutter 应用程序

创建指令化的 Flutter 应用程序要有这两步:

  1. 它启用了Flutter Driver的扩展
  2. 运行 App

test_driver/app.dart 文件里写下这两步:

import 'package:flutter_driver/driver_extension.dart';
import 'package:flutter_road_test/main.dart' as app;

void main() {
  // This line enables the extension
  enableFlutterDriverExtension();

  // Call the `main()` function of your app or call `runApp` with any widget you
  // are interested in testing.
  app.main();
}

第五步:写测试

现在有了指令化的 app, 我们要写测试了,测试需要下面四步:

  1. SeralizableFinders 来定位特定的 Widgets
  2. 测试前,在 setUpAll 方法中连接 app
  3. 测试一些重要的情况
  4. 当测试完,在 teardownAll 方法中断开连接
// Imports the Flutter Driver API
import 'package:flutter_driver/flutter_driver.dart';
import 'package:test/test.dart';

void main() {
  group('Counter App', () {
    // 通过 Finders 找到对应的 Widgets
    final counterTextFinder = find.byValueKey('counter');
    final buttonFinder = find.byValueKey('increment');

    FlutterDriver driver;

    // 连接 Flutter driver 
    setUpAll(() async {
      driver = await FlutterDriver.connect();
    });

    // 当测试完成断开连接
    tearDownAll(() async {
      if (driver != null) {
        driver.close();
      }
    });

    test('starts at 0', () async {
      // 用 `driver.getText` 来判断 counter 初始化是 0
      expect(await driver.getText(counterTextFinder), "0");
    });

    test('increments the counter', () async {
      // 首先,点击按钮
      await driver.tap(buttonFinder);

      // 然后,判断是否增加了 1
      expect(await driver.getText(counterTextFinder), "1");
    });
  });
}

第六步:运行测试

运行一个 Android 或者 iOS 模拟器或者连上自己的手机,然后从项目根目录下运行下面的命令:

flutter drive --target=test_driver/app.dart

命令的作用:

  1. 运行目标 app 并安装
  2. 启动 app
  3. 运行 app_test.dart 里的测试

结果:

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