Dart学习笔记(28):Test软件测试

发表于2018-07-04 13:24 阅读(41)

虽然前前后后接触了一段时间的编程,但是对软件测试这块还真是用之甚少,或零。但是如果下载相关的包或者源码,严谨的团队必包含test测试代码。写代码的时候,一般都是通过打桩式输出相关信息来测试。但是,那只是简单的验证目前写的代码有没有问题,而单元测试的重要性在于,你一次编写好的测试用例是否可以在日后随时随地地运行,以验证你本次所修改的代码是否影响到了以往的业务逻辑,为以后的开发提供支持。单元测试是一种保证你所写的代码在整个生命周期中都不会出Bug的防护墙,是具有重要价值的软件过程制品之一。

本文地址:http://www.cndartlang.com/874.html

正是因为可以在日后随时随地保证代码的质量,所以我们就可以随时随地地进行重构,而不需要害怕自己的修改是否会使以往的代码出现BUG。毕竟大部分情况下我们都是在维护一个大型的系统,大型系统的生命周期很漫长,中间会有新功能的添加和旧功能的重构,所以单元测试是非常需要的,应该引起重视。如果没有单元测试,很难保证代码在一两年之后还是清晰的,那就更谈不上“价值”二字了。

下面是Dart中test库的标准用法

1、编写Tests

通过调用顶层函数test()来进行测试,description参数是描述信息,body()为回调函数,断言使用expect()函数:

import "package:test/test.dart";

void main() {
  test("String.split() splits the string on the delimiter", () {
    var string = "foo,bar,baz";
    expect(string.split(","), equals(["foo", "bar", "baz"]));
  });

  test("String.trim() removes surrounding whitespace", () {
    var string = "  foo ";
    expect(string.trim(), equals("foo"));
  });
}

group()函数可以将单元测试组合在一起,每个组的描述信息在test()的描述信息之前显示:

import "package:test/test.dart";

void main() {
  group("String", () {
    test(".split() splits the string on the delimiter", () {
      var string = "foo,bar,baz";
      expect(string.split(","), equals(["foo", "bar", "baz"]));
    });

    test(".trim() removes surrounding whitespace", () {
      var string = "  foo ";
      expect(string.trim(), equals("foo"));
    });
  });

  group("int", () {
    test(".remainder() returns the remainder of division", () {
      expect(11.remainder(3), equals(2));
    });

    test(".toRadixString() returns a hex string", () {
      expect(11.toRadixString(16), equals("b"));
    });
  });
}

在使用expect()断言的时候,可以使用allOf函数进行复杂的验证:

import "package:test/test.dart";

void main() {
  test(".split() splits the string on the delimiter", () {
    expect("foo,bar,baz", allOf([
      contains("foo"),
      isNot(startsWith("bar")),
      endsWith("baz")
    ]));
  });
}

如果单元测试都使用了相同初始化代码,那么可以将代码放到setUp()和tearDown()函数中。setUp()会在执行每个group或者test之前调用回调函数,并在结束的时候调用tearDown()清除数据。并且,无论测试是否成功,都会调用tearDown():

import "package:test/test.dart";

void main() {
  var server;
  var url;
  setUp(() async {
    server = await HttpServer.bind('localhost', 0);
    url = Uri.parse("http://${server.address.host}:${server.port}");
  });

  tearDown(() async {
    await server.close(force: true);
    server = null;
    url = null;
  });

  // ...
}

2、运行

如果只是单个的dart文件,可以通过命令pub run test path/test.dart运行,如果是多个dart文件,可以使用命令pub run test path/dir,默认文件以 _test.dart 结尾。pub run test会自动test目录下,以 _test.dart 结尾的文件。

pub run test -n “xxx”命令会将字符串解释为正则表达式,对group或test测试的描述进行匹配,并运行该测试。而 -N 参数会将字符串解释为文本,如果描述包含该文本,则运行。

默认情况下pub run test是在Dart VM中运行,但你也可以通过命令pub run test -p chrome path/test.dart,在浏览器中进行测试,但测试结果同在Dart VM中一样,依然在命令行显示。事实上,你可以同时在两个平台上进行测试,命令为:pub run test -p “chrome,vm” path/test.dart

一般情况下我们也可以通过dart命令直接运行,如:dart test.dart。但是,这时候@TestOn、@Skip、@Timeout等注解或其他功能无效。

3、限制运行环境

一些测试文件只对特定的平台有意义,比如在引用dart:html或dart:io的时候,如果要测试某平台下文件系统的行为,或Chrome中的功能,@TestOn注解功能可以准确的对测试文件应该在什么环境下运行进行声明。该注解应在library或import声明之前。

@TestOn("vm")

import "dart:io";

import "package:test/test.dart";

void main() {
  // ...
}

@TestOn会调用”platform selector“选择器,参数如下:

  • vm: 在Dart VM中运行测试
  • dartium: 在Dartium中运行测试
  • content-shell: 在Headless Dartium content shell中运行
  • chrome: 在Chrome中运行测试
  • phantomjs: 在PhantomJS中运行测试
  • firefox: 在Mozilla Firefox中运行测试
  • safari: 在Apple Safari中运行测试
  • ie: 在IE中运行测试
  • dart-vm: 在Dart VM 、Dartium中运行测试,等同于!js
  • browser: 在浏览器中运行测试
  • js: 编译为JS进行测试,等同于!dart-vm
  • blink: 在使用Blink渲染引擎的浏览器中运行测试
  • windows: 在Windows下运行测试,如果vm是false,该参数也为false
  • mac-os: 在Mac OS下运行测试,如果vm是false,该参数也为false
  • linux: 在Linux下运行测试,如果vm是false,该参数也为false
  • android: 在Android下运行测试,如果vm是false,该参数也为false
  • ios: 在Windows下运行测试,如果vm是false,该参数也为false
  • posix: 在POSIX操作系统中运行测试,等同于!windows

选择器支持Boolean选择器语法,如果想在除Chrome外的其他浏览器运行测试,可以注解如下:

@TestOn("browser && !chrome")

4、异步测试

Dart中写异步代码是件非常容易的事,使用async/await关键字就可以简单地自动实现:

import "dart:async";

import "package:test/test.dart";

void main() {
  test("new Future.value() returns the value", () async {
    var value = await new Future.value(10);
    expect(value, equals(10));
  });
}

此外,也可以用completion()函数对Future进行匹配:

import "dart:async";

import "package:test/test.dart";

void main() {
  test("new Future.value() returns the value", () {
    expect(new Future.value(10), completion(equals(10)));
  });
}

异步模式匹配异常,匹配使用throwsA()函数:

import "dart:async";

import "package:test/test.dart";

void main() {
  test("new Future.error() throws the error", () {
    expect(new Future.error("oh no"), throwsA(equals("oh no")));
    //throwsStateError等同于Throws(isStateError)
    expect(new Future.error(new StateError("bad state")), throwsStateError);
  });
}

expectAsync()可以封装其他的函数,并且回调函数最多允许6个参数,并且不支持命名可选参数。expectAsync()主要有2个作用:

一是断言封装的函数执行确定的次数,如果执行的次数小于count,会一直等待,如果执行的次数小于count,则测试为fail。

二是断言封装的函数执行的最大次数,如果超过上限,则测试为fail。如果max为0(默认值),则执行的次数为count;如果max为-1,则执行的次数允许大于count。

import "dart:async";

import "package:test/test.dart";

void main() {
  test("Stream.fromIterable() emits the values in the iterable", () {
    var stream = new Stream.fromIterable([1, 2, 3]);

    stream.listen(expectAsync((number) {
      expect(number, inInclusiveRange(1, 3));
    }, count: 3));
  });
}

5、跳过测试

如果测试未正常工作,或者其他原因,需要跳过该测试文件,可以将其标注为Skip。如果你提供了一个reason理由,这个测试将不会允许,并且将reason打印出来。传递的字符串reason应该说明测试为什么被跳过。一般来说,跳过测试操作,表面测试应该允许,但暂时不工作。如果是因为平台环境的问题不兼容,应该使用@TestOn/testOn来代替。

@Skip("currently failing (see issue 1234)")

import "package:test/test.dart";

void main() {
  // ...
}

在group或test函数中,命名可选参数skip可以跳过某个测试,类型可以为Boolean,或者String以描述跳过的原因。

import "package:test/test.dart";

void main() {
  group("complicated algorithm tests", () {
    // ...
  }, skip: "the algorithm isn't quite right");

  test("error-checking test", () {
    // ...
  }, skip: true);
}

6、超时

默认情况下,测试的超时时间为30s,也可以通过@Timeout来注解整个测试的超时时间。例如在之前异步的例子:

@Timeout(const Duration(seconds: 5))

import "dart:async";

import "package:test/test.dart";

void main() {
  test("Stream.fromIterable() emits the values in the iterable", () {
    var stream = new Stream.fromIterable([1, 2, 3]);

    stream.listen(expectAsync((number) {
      expect(number, inInclusiveRange(1, 3));
    }, count: 4)); //stream只有3个元素,小于count,会一直阻塞
  });
}

除了通过注解设置全局超时时间,也可以用group或test的timeout参数设置局部超时时间,作用于该单元内部。这里需要注意的是静态函数Timeout.factor,表示上一层超时时间的倍数。例如:如果group嵌套了一个test,全局超时设置为10s,group设置为5s,test超时时间为factor(4),实际test的超时时间为20s。Timeout注解也有静态函数:@Timeout.factor()。

import "package:test/test.dart";

void main() {
  group("slow tests", () {
    // ...

    test("even slower test", () {
      // ...
    }, timeout: new Timeout.factor(2));
  }, timeout: new Timeout(new Duration(minutes: 1)));
}

7、特定平台下的配置

特殊情况下,针对不同的平台,可能配置也不同。比如Windows下运行速度可能慢一些,或DOM操作在Safari中有问题。针对这种情况,可以使用@OnPlatform注解或onPlatform 命名参数自动检测当前环境,并对测试文件、group、test进行设置。例如:

@OnPlatform(const {
  // Give Windows some extra wiggle-room before timing out.
  "windows": const Timeout.factor(2)
})

import "package:test/test.dart";

void main() {
  test("do a thing", () {
    // ...
  }, onPlatform: {
    "safari": new Skip("Safari is currently broken (see #1234)")
  });
}

无论是注解还是onPlatform参数,实际上都是做了一个Map映射,key为之前“限制运行环境”中提到的platform selector,value为应用配置,可以是其他注解类的实例,如Skip、Timeout,或者List。

如果多个key都能匹配,则相同的设置,最后一个value生效。对于全局特定平台的配置,也可以使用接下来要说的Whole-Package Configuration全局包设置。

8、Tag标签

Tags的参数是短字符串,并与test、group进行关联。他们没有任何内置的含义,却非常实用:你可以通过Tag,将自定义的配置与测试相关联,或者对测试进行过滤筛选,运行需要的测试,类似skip。

标签通过@Tags注解对文件生效,test、group可以使用tags参数进行设置,例如:

@Tags(const ["browser"])

import "package:test/test.dart";

void main() {
  test("successfully launches Chrome", () {
    // ...
  }, tags: "chrome");

  test("launches two browsers at once", () {
    // ...
  }, tags: ["chrome", "firefox"]);
}

如果标签并没有在包配置文件中声明,运行的时候会打印警告信息,所以请确保包含所有的标签。

测试可以命令行对测试进行过滤筛选,–tag或-t会运行指定tag的测试,–exclude-tags或-x会运行除指定tag外的测试。参数支持boolean selector语法,例如: -t “(chrome || firefox) && !slow”。

9、全局包配置

除了在测试文件中进行设置外,还可以利用YAML文件,通过Whole-Package Configuration进行全局设置。包配置应用于所有的测试文件,以及所有的包。当创建dart_test.yaml配置文件的时候,该文件可以包含相同的配置,例如:

# This package's tests are very slow. Double the default timeout.
timeout: 2s

# This is a browser-only package, so test on content shell by default.
platforms: [browser]

配置文件会设置新的默认值,但是仍然可以通过命令行进行重写,即命令行优先级>配置文件。例如在上面的例子中,可以使用 -p chrome参数覆盖替换browser。当然,包配置文件不仅仅是设置全局默认值。配置文件的参数如下:

9.1 单元设置

timeout
控制超时时间。none为不超时,数字加m为分,s为秒,x为倍数。例如:”1m 30s”。

skip
该参数控制是否跳过测试。它通常用于指定的标签,而不是顶层。类似与test()的skip参数,它可以是Boolean值,或者String。

tags:
  chrome:
    skip: "Our Chrome launcher is busted. See issue 1234."

test_on
声明测试支持的环境平台。通常用于指定的标签,确保某个功能在支持的环境运行。

tags:
  # Internet Explorer doesn't support promises yet.
  promises: {test_on: "browser && !ie"}

该参数也可以用于配置文件的顶层,以配置整个包支持的特点平台,如果在不支持的平台运行,会打印警告信息,并skip跳过。

# This package uses dart:io.
test_on: vm

9.2 运行设置

不同于单元设置,运行设置作用于全局,而不是某个test()、group(),因此在配置文件的顶层进行设置。

paths
默认执行测试的路径,通常是目录,也可以是指定的单一dart文件。为了兼容性,必须使用相对路径,默认值为”test”。

paths: [dart/test]

paths:
- test/instantaneous
- test/fast
- test/middling

filename
指定运行测试时,在目录(paths变量)中查找测试文件的,文件名匹配模式,支持Glob语法,默认值为”*_test.dart”。

filename: "test_*.dart"

platforms
指定测试默认的运行平台,如果命令行没有传递”-p”参数,则该变量生效。如果指定了多个值,则在所有指定的平台进行测试,默认值为[vm]。

platforms: [content_shell]

platforms:
- chrome
- vm

concurrency
设置运行测试时的并发数,提高运行测试的整体速度,默认值约为处理器数量的一半,如果设置为1,则一个时间只能运行一个测试。

concurrency: 3

reporter
设置运行结果的显示方式,值为:compact,不显示测试成功的结果;expanded,显示所有的测试结果;json,以Json格式显示所有的测试结果。在Windows下默认为expanded,其他环境默认为compact。

reporter: compact

9.3 设置标签

tags
该变量可应用于单元设置来设置所有给定的tag或一组tag。设置内容为map映射,类似于顶层设置,但是可能并不包含运行设置。

tags:
  # Integration tests need more time to run.
  integration:
  timeout: 1m

标签的设置可以为空。当测试代码中设置了某个标签,但是并没有在配置文件中设置,那么运行的时候会输出警告信息。当设置某标签的值为空后,表示该标签存在。

# We occasionally want to use --tags or --exclude-tags on these tags.
tags:
  # A test that spawns a browser.
  browser:

  # A test that spawns a firefox.
  firefox:

你可以使用Boolean选择器语法来定义多个标签,例如:

tags:
  # Tests that invoke sub-processes tend to be a little slower.
  ruby || python:
    timeout: 1.5x

该变量可以应用于任何层级的测试。如果一个group被标记为integration,它的timeout设置优先级大于suite(测试集,即一个dart文件)的timeout设置;如果group自身声明了timeout参数,那么它的优先级大于Tag标签。

如果多个标签应用于同一层级,并且配置有冲突,那么测试运行的结果不确定。

add_tags
添加附加的标签设置,它可以用来表示继承标签,并隐式添加另一个标签的设置,参数为字符串列表。

tags:
  # Any test that spawns a browser.
  browser:
    timeout: 2x

  # Tests that spawn specific browsers. These automatically get the browser tag
  # as well.
  chrome: {add_tags: [browser]}
  firefox: {add_tags: [browser]}
  safari: {add_tags: [browser]}
  ie: {add_tags: [browser]}

9.4 平台设置

on_os
特定操作系统对应的设置。

on_os:
  windows:
    timeout: 2x
  linux:
    timeout: 1x

on_platform
指定测试运行环境的设置。

on_platform:
  chrome || safari: {timeout: 2x}

看了文档,也简单的试了一下,除了on_os的key只能是windows、linux、max-os、android、ios外,和on_platform并没有什么区别。如果有什么不对的地方欢迎留言。

9.5 预设

presets
预设是配置信息的集合,可以在命令行中通过”-P”参数选择对应的预设。对于特殊或复杂的逻辑,预设比直接在命令行设置方便得多。命令行中可以设置多个预设,如果设置有冲突,则最后一个设置有效。

presets:
  # Use this when you need completely un-munged stack traces.
  debug:
    paths:
    - test/runner/browser
    - test/runner/pub_serve_test.dart
  xxx:
    timeout: 1.5x

presets变量也可以在单元设置中使用,例如:

tags:
  chrome:
    skip: "Our Chrome launcher is busted. See issue 1234."

    # 通过在命令行中设置参数 -P force 选择预设
    presets: {force: {skip: false}}

add_presets
添加附加的预设。当选择一个预设的时候,会隐式的选择它的add_presets。

presets:
  # Shortcut for running only browser tests.
  browser:
    paths: [test/runner/browser]

  # Shortcut for running only Chrome tests.
  chrome:
    filename: "chrome_*_test.dart"
    add_presets: [browser]

这里只是梳理了常用的test包的内容,详细的文档可以查看github或pub。