2606d,d多线程单元测试框架

原文

单元线程``2.2.4

几乎没有样板,使用内置单元测试块高级多线程``单元测试框架.

要使用该包,请在项根目录中执行以下命令:

cpp 复制代码
dub add unit-threaded

手动使用

请在你的项目依赖中填充以下依赖:

dub.json

cpp 复制代码
"unit-threaded": "~>2.2.4"

dub.sdl

cpp 复制代码
dependency "unit-threaded" version="~>2.2.4"

unit-threaded(线单)包提供可单独使用的子包:

1,线单:from,依赖保存声明工具

2,线单:exception,异常

3,线单:assertions,用来测试自定义断定

4,单元线程:运行器,测试运行器

5,单元线程:模拟,模拟功能

6,单元线程:整合,整合测试工具

7,线单:property,基于属性的测试工具

8,线单:autorunner,自动运行dub root项目所有模块的所有测试.

9,线单:bebehavior,BDD输出格式化

单元线程

我用DConf2016的说明单元线程讲解.

适合D语言的多线程高级单元测试框架,这里.

增强D的单元测试块包括:

1,可命名测试并单独执行.

2,自定义断定更好地报告错误(如1.should==2)

3,默认在线程中运行

4,用来测试自定义UDA

5,基于属性的测试

6,模拟

快速入门配音

注意:虽然这样入门很简单,但也会增加构建时间,可能会遇见一些特例.下面说明了如何手动操作.

Dubdub test运行测试.可惜,因为D的编译时``反射特性,使用该库必须有一个列举所有需要反省的模块测试运行器文件.

因为这是项麻烦且易于自动化的任务,线单有叫gen_ut_maindub配置来实现它.要在dub项目中使用单元线程,可用如下单元测试配置,如本dub.json所示:

cpp 复制代码
{
    "name": "myproject",
    "targetType": "executable",
    "targetPath": "bin",
    "configurations": [
        { "name": "executable" },
        {
            "name": "unittest",
            "targetType": "executable",
            "preBuildCommands": ["$DUB run --compiler=$$DC unit-threaded -c gen_ut_main -- -f bin/ut.d -d $DUB"],
            "mainSourceFile": "bin/ut.d",
            "excludedSourceFiles": ["src/main.d"],
            "dependencies": {
                "unit-threaded": "*"
            }
        }
    ]
}

使用dub.sdl:

cpp 复制代码
configuration "executable" {
}

configuration "unittest" {
    dependency "unit-threaded" version="*"
    mainSourceFile "bin/ut.d"
    excludedSourceFiles "src/main.d"
    targetType "executable"
    preBuildCommands "$DUB run --compiler=$$DC unit-threaded -c gen_ut_main -- -f bin/ut.d -d $DUB"
}

excludedSourceFiles是为了避免编译包含主要函数的文件,以避免链接错误.或可用版本去掉"真正的"主文件:

cpp 复制代码
version(unittest) {
    import unit_threaded;
    mixin runTestsMain!(
        "module1",
        "module2",
//...
    );
} else {
    void main() {
//...
    }
}

用测试手动列举D模块

另,推荐手动在单元测试主函数列举所有带测试的模块.这里有一个插件:

cpp 复制代码
import unit_threaded;
mixin runTestsMain!(
    "mypkg.mymod0",
    "mypkg.mymod1",
//...
);

现在会在线程中运行你的单元测试块,且可单独运行.要为每个单位测试命名,只需附加UDA串:

cpp 复制代码
@("Test that 2 + 3 is 5")
unittest {
    assert(2 + 3 == 5);
}

还可对单元测试,设置多个配置,如一个使用标准D运行时``单元测试运行器,另一个使用单元线程的:

cpp 复制代码
"configurations": [
    {"name": "ut_default"},
    {
      "name": "unittest",
      "preBuildCommands: ["$DUB run --compiler=$$DC unit-threaded -c gen_ut_main -- -f bin/ut.d -d $DUB"],
      "mainSourceFile": "bin/ut.d",
      ...
    }
]

在此,如果不使用该库,dub test -c ut_default正常运行,dub 测试则带单元线程测试运行器运行.

要使用单元线程断定或基于UDA的功能,必须导入该库:

cpp 复制代码
//这里不要用`"version(unittest)"`,
//如果有人依赖你的代码而不依赖`单元线程`,
//他们就无法测试自己的代码!
version(TestingMyCode) { import unit_threaded; }
else                   { enum ShouldFail; }
//所以生产构建编译了

int adder(int i, int j) { return i + j; }
@("Test adder") unittest {
    adder(2, 3).shouldEqual(5);
}
@("Test adder fails", ShouldFail) unittest {
    adder(2, 3).shouldEqual(7);
}

如果如上对单元线程,使用自定义``dub配置,可在Have_unit_threaded使用版本块(dub给构建添加了).

自定义断定

如下:

cpp 复制代码
1.should == 1;
1.should.not == 2;
1.should in [1, 2, 3];
4.should.not in [1, 2, 3];

void funcThrows() { throw new Exception("oops"); }
funcThrows.shouldThrow;
//或用`.be`
1.should.be == 1;
1.should.not.be == 2;
1.should.be in [1, 2, 3];
4.should.not.be in [1, 2, 3];

//我知道这是滥用`重载符号`.我依然喜欢它.
[1, 2, 3].should ~ [3, 2, 1];
[1, 2, 3].should.not ~ [1, 2, 2];
1.0.should ~ 1.0001;
1.0.should.not ~ 2.0;

更多内容见unit_threaded.should模块.

快速编译模式

快速编译模式.按unitThreadedLight设置版本,编译速度会快很多,但不会报告错误,去掉了某些功能.是实验性支持.

高级用法:属性

@ShouldFail来装饰期望会失败的测试,并可传递串来解释原因.@ShouldFail应该比@HiddenTest更受欢迎.如果已修复相关漏洞或完成尚未实现的功能,测试就会失败,这样更难忽视和忘记测试.

因为被测代码可能不是线安的,可在测试中使用@Serial属性.这会顺序执行同一模块中所有带该属性的测试,避免相互交错.

虽然不是最佳做法,但有时测试结果不稳定.建议按权宜之计修复测试,可用@Flaky,UDA重复测试,默认最多为10次.可传递一个数字(如@Flaky(12))来自定义;

可用@Name,UDA来代替普通串来命名单元测试块.

线单使用D包和模块系统,这样可选择要运行的测试子集.然而,有时不同模块中的测试,会涉及交叉问题,因此建议标明该分组这样仅选择这些测试.

可用@Tags,UDA来实现它.针对测试可用多个标签:

cpp 复制代码
@Tags("foo", "tagged")
unittest { ... }

通过选择带标签或不带标签的测试限制执行哪些测试,测试运行器可用串来标记测试:

cpp 复制代码
./ut @foo ~@bar

它会运行所有带"福"标签,但无"栏"标签的测试.

可在模块中,给自由函数附加@安装@ShutdownUDA.如果这样,它们会在复合测试中,每个单元测试前后运行.

复合一般是个模块,但可用虚结构,来只对部分测试应用@安装@关机功能:

cpp 复制代码
struct Namespace {
    @Setup void setup() { writelnUt("Setup should run exactly twice"); }
    @("With setup1")
    unittest {
    }
    @("With setup2")
    unittest {
    }
}
@("No Setup")
unittest {
}

注意:@安装@关机仅适合当前域.如果你在结构里写更多单元测试,就没用.

基于属性的测试

当前已有初步支持基于属性的测试.要检查属性,可用unit_threaded.property中返回的函数的检查函数:

cpp 复制代码
check!((int a) => a % 2 == 0);

上述示例显然会失败.默认,检查会用100随机值运行属性函数,给它传递一个不同的运行时参数来更改:

cpp 复制代码
check!((int a) => a % 2 == 0)(10_000);
//还是会失败

如果如上使用编译时代理,必须显式说明输入参数的类型.只要每个参数当前支持的类型之一,即可使用多个参数.

模拟

类和接口可如下模拟:

cpp 复制代码
interface Foo { int foo(int, string); }
int fun(Foo f, int i, string s) { return f.foo(i * 2, s ~ "stuff"); }
auto m = mock!Foo;
m.expect!"foo";
fun(m, 3, "bar");
m.verify;
//如果未调用,则抛

要检查传入的值,给它们传递期望:

cpp 复制代码
m.expect!"foo"(6, "barstuff");
fun(m , 3, "bar");
m.verify;

或先调用期望然后验证,或在结尾调用``expectCalled:

cpp 复制代码
fun(m, 3, "bar");
m.expectCalled!"foo"(6, "barstuff");

除非调用returnValue(其是可变变量),返回值T.init:

cpp 复制代码
m.returnValue!"foo"(2, 3, 4);
assert(fun(m, 3, "bar") == 2);
assert(fun(m, 3, "bar") == 3);
assert(fun(m, 3, "bar") == 4);
assert(fun(m, 3, "bar") == 0);

也可模拟结构:

cpp 复制代码
int fun(T)(T f, int i, string s) { return f.foo(i * 2, s ~ "stuff"); }
auto m = mockStruct(2, 3, 4);
//整数为`返回值`(无需传递他们)
assert(fun(m, 3, "bar") == 2);
m.expectCalled!"foo"(6, "barstuff");

如果需要结构,对不同函数,会返回不同类型:

cpp 复制代码
    auto m = mockStruct!(ReturnValues!("length", 5, 3), ReturnValues!("greet", "hello", "g'day"));
    m.length.shouldEqual(5);
    m.length.shouldEqual(3);
    m.greet.shouldEqual("hello");
    m.grett.shouldEqual("g'day");

总是抛的结构:

cpp 复制代码
{
    auto m = throwStruct;
    m.foo.shouldThrow!UnitTestException;
}
{
    auto m = throwStruct!MyException;
    m.foo.shouldThrow!MyException;
}

命令行参数

要按单线程模式运行,使用-s.

测试,支持使用-d开关调试打印.测试可用writelnUt函数打印调试输出.

测试可随机顺序运行,而不是线程中.为此,可用-r选项.会打印一个种子,这样可用种子选项重复运行.即在单线程中运行.

整合测试与沙箱环境

如果想写可从文件系统读写的测试,可用unit_threaded.integrationSandbox结构,比如这样:

cpp 复制代码
with(immutable Sandbox()) {
    writeFile("foo.txt", "foobarbaz\ntoto");
//对行也可传递`串[]`
    shouldExist("foo.txt");
    shouldNotExist("bar.txt");
    shouldEqualLines("foo.txt", ["foobarbaz", "toto"]);
}

默认沙箱主路径是tmp/unitthread,但可调用Sandbox.setPath来更改.

测试注册与测试运行器

示例目录里有两个示例,一个带成功单元测试,另一个带失败的,用来展示各种情况下的输出.因为D包的工作原理,它们必须从仓库的顶级目录中运行.

两个示例输出中可见(example.tests.pass_tests.unittest及同目录中的example_fail),自动包含内置的D单元测试块.

他们会自动生成一个名字.用户可用串UDA或包含的UDA``@名,@Name装饰来指定名.

运行测试最好是像失败的示例代码一样:在runner.d中,用包含测试的模块编译时参数(串)插件进入runTestsMain().

无需注册测试.注册是隐式的,并且每个单元测试块都注册了.

一般如上dub配置所示,在调用runTestsrunTestsMain时,需要指定要反省的模块.

可用@HiddenTest属性隐藏测试.即默认不会执行该测试,但可用命令行参数传递其名来执行.HiddenTest会取编译时串来列举隐藏测试的原因.

这一般是个漏洞标识,但也可以是用户想要的任何东西.

因为D包只是目录,编译器编译时无法读取文件系统,因此无法自动在包中添加所有测试.

为了缓解它,避免手动写入包含测试的所有模块名,有一个叫gen_ut_maindub配置,按命令行工具运行单元线程为你写文件.

相关项目

1,dunit:D的x单元测试框架,这里

2,DMocksrevived:一个允许模拟接口或类模拟对象框架,这里

3,deject:自动依赖注入,这里

4,specd:受specs2ScalaTest启发的单元测试框架,这里这里

5,DUnit:测试断定工具包支持模拟模板插件.这里