flutter_tests 自动化测试

267 阅读5分钟

单元测试

Flutter apps test 分为 unittest、widget test、integration test

  • unittest

    • 入门参考地址:docs.flutter.dev/cookbook/te…

    • 进阶参考:pub.dev/packages/te…

    • 以上两个建议都看,很快就能看完

    • Integration Testing:docs.flutter.dev/testing/int…

    • 添加依赖库

      • dev_dependencies: test: <latest_version>

        • latest_version 代表版本,版本查看地址如下:

        pub.dev/packages/te…

      • image.png

      • 点击复制按钮,出现 提示,转移到目标yaml文件中粘贴即可:

      • image.png

      • 使用时注意提示:

        The current Dart SDK version is 2.17.0. Because my_flutter_driver depends on test >=1.21.5 which requires SDK version >=2.18.0-146.0.dev <3.0.0, version solving failed. pub get failed (1; Because my_flutter_driver depends on test >=1.21.5 which requires SDK version >=2.18.0-146.0.dev <3.0.0, version solving failed.)

        我最终用了1.21.0:

      • image.png

  • 需要有两个文件,目标文件,和执行测试文件。

    • counter.dart 是要被测试的目标文件,counter_test.dart 是执行测试的文件。

      • class Counter {
          int value = 0;
        
          void increment() => value++;
        
          void decrement() => value--;
        }
        
    • In this example, create two files: counter.dart and counter_test.dart.

      The counter.dart file contains a class that you want to test and resides in the lib folder. The counter_test.dart file contains the tests themselves and lives inside the test folder.

      In general, test files should reside inside a test folder located at the root of your Flutter application or package. Test files should always end with _test.dart, this is the convention used by the test runner when searching for tests.

      When you’re finished, the folder structure should look like this:

    • 意思是,我们应该在lib文件夹下创建目标文件counter.dart,在test文件夹下,创建所有的要执行测试的文件

      counter_app/
        lib/
          counter.dart
        test/
          counter_test.dart
      
    • test 文件夹是自动生成的,或许flutter项目创建时,也或许是test被引入时。

    • image.png

    • 在执行测试文件中:

      import 'package:test/test.dart';
      
    • 在我的flutter项目中并未找到,我用如下代替:

    • import 'package:flutter_test/flutter_test.dart';
      
  • 命令行运行:

    /Users/qingweiliu/Library/flutter/bin/flutter --no-color test --machine --start-paused --plain-name description test/counter_test.dart
    
  • image.png

  • 右键直接运行:

  • image.png

  • 测试通过后:

    • image.png
      • 左侧框是测试对象信息,右侧是测试通过的梳理。

上述写法其实是有错误的(但是官方demo里就是这么写的,不知现在更新了没有),只要不出现变编译问题,测试结果都是通过,我们修改如下:

test('description', (){
    final counter = Counter();
    counter.increment();
    expect(counter.value, 2, reason: "start at 1 返回值错误");
  });

重新运行看结果:错误的提示信息也已打印出来 "sart at 1 返回值错误"

image.png

  • Combine multiple tests in a group:同时进行多项测试(一个测试失败不影响下一个继续)

    • 注意 test.dart 引用失败用flutter_test(import 'package:flutter_test/flutter_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);
          });
        });
      }
      
    • 注意以下三种写法的问题:第三种写法,不会爆出异常,而是测试通过。

    • group('Counter', () {
        test('value should start at 0', () => {
          expect(Counter().value, 0, reason: "start at 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, 20, reason: "返回值错误");
        });
      });
      
    • IDE运行:右侧 Run 'Counter' 或者点击左侧运行按钮

      • image.png
      • Tests faile:1,passed:2, 测试通过2个,有一个case测试失败,未达到预期。左下角是每一个case 测试情况。差号的case 就是没通过。
      • image.png
  • 命令行执行测试文件 .dart

    • flutter test test/counter_test.dart
    • 会将main() 方法里面的四个test都走一遍,这个时候,不需要group,四个test并列也可以了。
    • import 'package:flutter_test/flutter_test.dart';
      import '../lib/counter.dart';
      
      void main() {
        test('description', () => (){
          final counter = Counter();
          counter.increment();
          expect(counter.value, 1);
        });
      
        group('Counter', () {
          test('value should start at 0', () => {
            expect(Counter().value, 0, reason: "start at 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, 20, reason: "返回值错误");
          });
        });
      
      }
      
    • 命令行运行后效果:
    • image.png
    • 最终日志意思是:3个测试成功,2个测试失败。
  • 指定测试文件中的description执行测试

    • flutter --no-color test --machine --start-paused --plain-name Counter test/counter_test.dart
      
    • --plain-name:这个命令行,只会执行counter_test.dart 里面的 group('Counter') 的测试。

    • 测试尝试如下:测试通过2例,1个case测试未通过。

      • image.png

Widgets_Tests

参考文档:docs.flutter.dev/cookbook/te…

测试某个widget:

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

相较于test,testWidgets多了tester,允许对widget进行更多操作。

pumpWidget:

对目标widget进行重建

tester.pump();//立即刷新

pump(Duration duration): 刷新tester里面的东西,比如刷新widget

官方解释:

Repeatedly calls pump() with the given duration until there are no longer any frames scheduled. This, essentially, waits for all animations to complete.

个人感觉上面这句话比代码注释里解释的要好。

tester.pumpAndSettle();//刷新所有;比如多个widget、frames等

查找WidgetsTree里面某个widget:

testWidgets('MyWidget has a title and message', (tester) async {
    await tester.pumpWidget(const MyWidget(title: 'T', message: 'M'));
    final titleFinder = find.text('T');
    
    expect(titleFinder, findsOneWidget);//有且仅有一个目标widget
    expect(titleFinder, findsNothing);//一个都没有
    expect(titleFinder, findsWidgets);//有一个或者多个
    expect(titleFinder, findsNWidgets(10));//有10个
    expect(find.text('0'), matchesGoldenFile(key, version: 10));//
}

matchesGoldenFile:

matchesGoldenFile(key, version: 10):Verifies that a widget’s rendering matches a particular bitmap image (“golden file” testing).

/// The [key] may be either a [Uri] or a [String] representation of a URL.

 /// The [version] is a number that can be used to differentiate historical
/// golden files. This parameter is optional.

matchesGoldenFile 一般与expectLater 搭配。

 /// await expectLater(
///   find.text('Save'),
///   matchesGoldenFile('save.png'),
/// ); 
 /// await expectLater(
///   find.byType(MyWidget),
///   matchesGoldenFile('goldens/myWidget.png'),
/// ); 

测试WidgetsTree里面某个widget点击事件:

await tester.tap(find.byIcon(Icons.add));

其它:

tapAt(): 指定位置触摸; tap() 默认执行于中心位置:topAt(location:center).

press(): 与tap()不同的是,返回gesture对象,并允许继续操作。tap() 是一个void函数。

更多:

longPress()、longPressAt()、fling()、flingFrom()、drag()、dragFrom()、timeDrag()、timeDragFrom()。

filing() 会引发pump刷新。

Integration_tests:

lib/
  ...
integration_test/
  foo_test.dart
  bar_test.dart
test/
  # Other unit tests go here.

运行指定的test.dart

flutter test integration_test/foo_test.dart -d <DEVICE_ID>

运行所有的:

flutter test integration_test

详细使用参见:github.com/flutter/flu…

可以在TestLab里面进行集成测试:

Migrating from flutter_driver: 可忽略

参见:docs.flutter.dev/testing/int…

Example:

参见:github.com/flutter/web…

driver:测试点击:

test('tap on the first item (Alder), verify selected', () async {
  // find the item by text
  final item = find.text('Alder');

  // Wait for the list item to appear.
  await driver.waitFor(item);

  // Emulate a tap on the tile item.
  await driver.tap(item);

  // Wait for species name to be displayed
  await driver.waitFor(find.text('Alnus'));

  // 'please select' text should not be displayed
  await driver
      .waitForAbsent(find.text('Please select a plant from the list.'));
});

integration_test 测试点击:

testWidgets('tap on the first item (Alder), verify selected',
    (tester) async {
  await tester.pumpWidget(const PlantsApp());

  // wait for data to load
  await tester.pumpAndSettle();

  // find the item by text
  final item = find.text('Alder');

  // assert item is found
  expect(item, findsOneWidget);

  // Emulate a tap on the tile item.
  await tester.tap(item);
  await tester.pumpAndSettle();

  // Species name should be displayed
  expect(find.text('Alnus'), findsOneWidget);

  // 'please select' text should not be displayed
  expect(find.text('Please select a plant from the list.'), findsNothing);
});

flutter intergration_test 与 driver 应用到实际项目中:

可以参考:docs.qq.com/doc/DUVJDc0…