unittest 包

介绍

unittest 包使用元编程语法来支持单元测试功能,用于编写和运行可重复的测试用例,并进行结构化测试。 仓颉编译器提供 --test 编译选项来自动组织源码中的测试用例以及生成可执行程序的入口函数。

宏功能介绍

@Test

@Test 宏应用于顶级函数或顶级类,使该函数或类转换为单元测试类。 如果是顶级函数,则该函数新增一个具有单个测试用例的类提供给框架使用,同时该函数仍旧可被作为普通函数调用。

标有 @Test 的类必须满足以下条件:

  1. 它必须有一个无参构造函数
  2. 不能从其他类继承

实现说明:@Test 宏为任何用它标记的类引入了一个新的基类:unittest.TestCasesunittest.TestCases 的所有公共和受保护成员(请参阅下面的 API 概述)将在标有 @Test 的类或函数中变得可用,包括两个字段: 1. 包含此测试的 TestContext 实例的 ctx。 2. 包含类的名称的 name 。 单元测试框架的用户不应修改这些字段,因为这可能会导致不可预期的错误。

@TestCase

@TestCase 宏用于标记单元测试类内的函数,使这些函数成为单元测试的测试用例。 标有 @TestCase 的函数必须满足以下条件:

  1. 该类必须用 @Test 标记
  2. 该函数返回类型必须是 Unit
@Test
class Tests {
    @TestCase
    func fooTest(): Unit {...}
}

测试用例可能有参数,在这种情况下,开发人员必须使用参数化测试 DSL 指定这些参数的值:

@Test[x in source1, y in source2, z in source3]
func test(x: Int64, y: String, z: Float64): Unit {}

此 DSL 可用于 @Test@RawBench@Bench@TestCase 宏,其中 @Test 仅在顶级函数上时才可用。如果测试函数中同时存在 @Bench@TestCase ,则只有 @Bench 可以包含 DSL 。 在 DSL 语法中,in 之前的标识符(在上面的示例中为 xyz )必须直接对应于函数的参数,参数源(在上面的示例中为source1source2source3 ) 是任何有效的仓颉表达式(该表达式类型必须实现接口 DataStrategy<T> ,详见下文)。 参数源的元素类型(此类型作为泛型参数 T 提供给接口 DataStrategy<T> )必须与相应函数参数的类型严格相同。

目前,参数化测试最多支持 5 个参数,支持的参数源类型如下:

  • Arrays: x in [1,2,3,4]
  • Ranges: x in 0..14
  • 随机生成的值: x in random()
  • 从 json 文件中读取到的值: x in json("filename.json")
  • 从 csv 文件中读取到的值: x in csv("filename.csv")

高级用户可以通过定义自己的类型并且实现 DataStrategy<T> 接口来引入自己的参数源类型。有关详细信息,请参阅 “高级特性” 章节。

使用 random() 的随机生成函数默认支持以下类型:

  • Unit
  • Bool
  • 所有内置的 integer 类型(包含有符号和无符号)
  • 所有内置的 float 类型
  • Ordering
  • 所有已支持类型的数组类型
  • 所有已支持类型的 Option 类型

若需要新增其他的类型支持 random() ,可以让该类型扩展 unittest.prop_test.Arbitrary 。有关详细信息,请参阅 “高级特性” 章节。

在参数有多个值时,beforeEach / afterEach 不会在不同值下重复执行而仅会执行一次。若确实需要在每个值下做初始化和去初始化,需要在测试主体中写。对于性能测试场景,应使用 @RawBench 。暂时不对此类情况提供特殊 API ,因为大多数情况下,此类代码取决于具体参数值。

@Bench

@Bench 宏用于标记要执行多次的函数并计算该函数的预期执行时间

此类函数将分批执行,并针对整个批次测量执行时间。这种测量将重复多次以获得结果的统计分布,并将计算该分布的各种统计参数。 当前支持的参数如下:

  • 中位数
  • 用作误差估计的中位数 99% 置信区间的绝对值
  • 中位数 99% 置信区间的相对值
  • 平均值

参数化的 DSL 与 @Bench 结合的示例如下,具体语法与规则详见《 @TestCase 宏》章节:

func sortArray<T>(arr: Array<T>): Unit 
        where T <: Comparable<T> {
    if (arr.size < 2) { return }
    var minIndex = 0
    for (i in 1..arr.size) {
        if (arr[i] < arr[minIndex]) { 
            minIndex = i   
        }
    }
    (arr[0], arr[minIndex]) = (arr[minIndex], arr[0])
    sortArray(arr[1..])
}

@Test 
@Configure[baseline: "test1"]
class ArrayBenchmarks{
    @Bench
    func test1(): Unit 
    {
        let arr = Array(10) { i: Int64 => i }
        sortArray(arr)
    }

    @Bench[x in 10..20]
    func test2(x:Int64): Unit 
    {
        let arr = Array(x) { i: Int64 => i.toString() }
        sortArray(arr)
    }
}

输出如下, 增加 Args 列,列举不同参数下的测试数据,每个参数值将作为一个性能测试用例输出测试结果,多个参数时将列举全组合场景:

--------------------------------------------------------------------------------------------------
TP: default, time elapsed: 68610430659 ns, Result:
    TCS: ArrayBenchmarks, time elapsed: 68610230100 ns, RESULT:
    | Case   | Args   |   Median |       Err |   Err% |     Mean |
    |:-------|:-------|---------:|----------:|-------:|---------:|
    | test1  | -      | 4.274 us | ±2.916 ns |  ±0.1% | 4.507 us |
    |        |        |          |           |        |          |
    | test2  | 10     | 6.622 us | ±5.900 ns |  ±0.1% | 6.670 us |
    | test2  | 11     | 7.863 us | ±5.966 ns |  ±0.1% | 8.184 us |
    | test2  | 12     | 9.087 us | ±10.74 ns |  ±0.1% | 9.918 us |
    | test2  | 13     | 10.34 us | ±6.363 ns |  ±0.1% | 10.28 us |
    | test2  | 14     | 11.63 us | ±9.219 ns |  ±0.1% | 11.67 us |
    | test2  | 15     | 13.05 us | ±7.520 ns |  ±0.1% | 13.24 us |
    | test2  | 16     | 14.66 us | ±11.59 ns |  ±0.1% | 15.53 us |
    | test2  | 17     | 16.21 us | ±8.972 ns |  ±0.1% | 16.35 us |
    | test2  | 18     | 17.73 us | ±6.288 ns |  ±0.0% | 17.88 us |
    | test2  | 19     | 19.47 us | ±5.819 ns |  ±0.0% | 19.49 us |
    Summary: TOTAL: 11
    PASSED: 11, SKIPPED: 0, ERROR: 0
    FAILED: 0
--------------------------------------------------------------------------------------------------

@RawBench

@Bench 相同,但可以访问更高级的 API 。被 @RawBench 修饰的函数主体将被执行一次,并且应使用 RawBencher 类进行相关配置。 @RawBench 宏不能与 @TestCase@Bench 一起应用在同一个测试用例上。

@Configure

@Configure 宏为测试类或测试函数提供配置参数。它可以放置在测试类或测试函数上。它具有以下形式:

@Configure[parameter1: <value1>,parameter2: <value2>] 其中 parameter1 是仓颉标识符,value 是任何有效的仓颉表达式。 value 可以是常量或在标有 @Configure 的声明范围内有效的任何仓颉表达式。 如果多个参数具有不同的类型,则它们可以有相同的名称。如果指定了多个具有相同名称和类型的参数,则使用最新的一个。

目前支持的配置参数有:

  • randomSeed: 类型为 Int64, 为所有使用随机生成的函数设置起始随机种子。
  • generationSteps: 类型为 Int64 :参数化测试算法中的生成值的最大步数。
  • reductionSteps :类型为 Int64: 参数化测试算法中的缩减值的最大步数。 注意:以下参数一般用于被 @Bench 修饰的 Benchmark 测试函数。
  • explicitGC :类型为 ExplicitGcType: Benchmark 函数测试期间如何调用 GC。默认值为 ExplicitGcType.Light
  • baseline :类型为 String : 参数值为 Benchmark 函数的名称,作为比较 Benchmark 函数执行结果的基线。该结果值将作为附加列添加到输出中,其中将包含比较结果。
  • batchSize :类型为 Int64 或者 Range<Int64> : 为 Benchmark 函数配置批次大小。默认值是由框架在预热期间计算得到。
  • minBatches :类型为 Int64 : 配置 Benchmark 函数测试执行期间将执行多少个批次。默认值为 10
  • minDuration :类型为 Duration : 配置重复执行 Benchmark 函数以获得更好结果的时间。默认值为 Duration.second * 5
  • warmup :类型为 Duration 或者 Int64 : 配置在收集结果之前重复执行 Benchmark 函数的时间或次数。默认值为 Duration.second 。当值为 0 时,表示没有 warmup , 此时执行次数按用户输入的 batchSizeminBatches 计算得到,当 batchSize 未指定时将抛出异常。
  • measurement:类型为 Measurement :描述性能测试需要收集的信息。默认值为 TimeNow() ,它在内部使用 DateTime.now() 进行测量。

用户可以在 @Configure 宏中指定其他配置参数,这些参数将来可能会用到。 如果测试类使用 @Configure 宏指定配置,则该类中的所有测试函数都会继承此配置参数。 如果此类中的测试函数也标有 @Configure 宏,则配置参数将从类和函数合并,其中函数级宏优先。

@Types

@Types 宏为测试类或测试函数提供类型参数。它可以放置在测试类或测试函数上。它具有以下形式:

@Types[Id1 in <Type1, Type2, Type3>, Id2 in <Type4, Type5> ...] 其中 Id1Id2... 是有效类型参数标识符,Type1Type2Type3...是有效的仓颉类型。

@Types 宏有以下限制:

  • 必须与 @Test, @TestCase@Bench 宏共同使用。
  • 一个声明只能有一个 @Types 宏修饰。
  • 该声明必须是具有与 @Types 宏中列出的相同类型参数的泛型类或函数。
  • 类型列表中列出的类型不能相互依赖,例如 @Types[A in <Int64, String>, B in <List<A>>] 将无法正确编译。但是,在为该类内的测试函数提供类型时,可以使用为测试类提供的类型。例如:
@Test
@Types[T in <...>]
class TestClass<T> {
    @TestCase
    @Types[U in <Array<T>>]
    func testfunc<U>() {}
}

该机制可以与其他测试框架功能一起使用,例如 @Configure 等。

@Skip

@Skip[expr] 修饰已经被 @TestCase 修饰的函数

  1. expr 暂只支持 true ,表达式为 true 时,跳过该测试,其他均为 false
  2. 默认 exprtrue@Skip[true] == @Skip

@Timeout

@Timeout[expr] 指示测试应在指定时间后终止。它有助于测试可能运行很长时间或陷入无限循环的复杂算法。 expr 的类型应为 std.time.Duration 。 其修饰测试类时为每个相应的测试用例提供超时时间。

@Parallel

@Parallel 宏可以修饰测试类。被 @Parallel 修饰的测试类中的测试用例,将被分别独立到不同的进程中并行运行。

  1. 所有相关的测试用例应该各自独立,不依赖于任何可变的共享的状态值。
  2. beforeAll()afterAll() 应该是可重入的,以便可以在不同的进程中多次运行。
  3. 需要并行化的测试用例本身应耗时较长。否则并行化引入的多次 beforeAll()afterAll() 可能会超过并行化的收益。
  4. 不允许与 @Bench 同时使用。由于性能用例对底层资源敏感,用例是否并行执行,将影响性能用例的结果,因此禁止与 @Bench 同时使用。

@Assert

@Assert 声明 Assert 断言,测试函数内部使用,断言失败停止用例

  1. @Assert(leftExpr, rightExpr) ,比较 leftExprrightExpr 值是否相同
  2. @Assert(condition: Bool) ,比较 condition 是否为 true ,即 @Assert(condition: Bool) 等同于 @Assert(condition: Bool, true)

@PowerAssert

@PowerAssert@Assert 类似, 但是将打印更详细的中间值和异常信息。

打印的详细信息如下:

REASON: `foo(10, y: test   + s) == foo(s.size, y: s) + bar(a)` has been evaluated to false
Assert Failed: `(foo(10, y: test   + s) == foo(s.size, y: s) + bar(a))`
                |          |        |_||  |   |_|    |   |_|| |   |_||
                |          |       "123"  |  "123"   |  "123" |    1 |
                |          |__________||  |   |______|      | |______|
                |            "test123" |  |       3         |    33  |
                |______________________|  |_________________|        |
                            0             |        1                 |
                                          |__________________________|
                                                       34

--------------------------------------------------------------------------------------------------

@Expect

@Expect 声明 Expect 断言,测试函数内部使用,断言失败继续执行用例

  1. @Expect(leftExpr, rightExpr) ,比较 leftExprrightExpr 是否相同
  2. @Expect(condition: Bool) ,比较 condition 是否为 true ,即 @Expect(condition: Bool) 等同于 @Expect(condition: Bool, true)

编译选项 --test 介绍

--test 是仓颉编译器 cjc 的内置编译选项

  1. 测试用例编译,配合 unittest 库使用
  2. 条件编译,测试代码应放在以 _test.cj 结尾的文件中,如果包编译时未使用 --test 则忽略该文件

使用方法

使用 --test 编译测试文件

例如,有如下一段测试代码:

@Test
public class TestA {
    @TestCase
    public func case1(): Unit {
        print("case1\n")
    }
}
cjc test.cj -o test --test && ./test
  • test.cj 是含有测试用例的仓颉文件
  • test 是编译输出的可执行程序
  • --test 是编译选项,当使用 --test 选项编译时,程序入口不再是 main ,而是由编译器生成的入口函数。
  • ./test 执行测试用例

使用 --test 组织包测试

目录结构:

application
├── pkgc
|   ├── a1.cj
|   └── a1_test.cj
|   └── a2.cj
|   └── a2_test.cj
  • a1_test.cj 是 a1.cj 的单元测试
  • a2_test.cj 是 a2.cj 的单元测试
cjc -p pkgc -o test --test && ./test
  • -p 整包编译选项
  • pkgc 是含有测试用例的包路径
  • test 是编译输出的可执行程序
  • --test 是编译选项,当使用 --test 选项编译时,-程序入口不再是 main ,而是由编译器生成的入口函数
  • ./test 执行所有测试用例

unittest 没有严格限制只能测试同 package 的内容,但通常情况下由于单元测试需要测试仅包内可见的内容,推荐使用如上形式组织 package 的单元测试。

编译器约束 (仅在使能 --test 时生效)

  1. @Test 宏不能修饰非 TopLevel 的函数。
  2. @Test 宏不能修饰泛型类。
  3. @Test 宏修饰的类必须包含一个无参数的构造函数。
  4. @Testcase 宏只能修饰返回值类型为 Unit 的成员函数。
  5. @Testcase 宏不能修饰 foreign 函数。
  6. @Testcase 宏不能修饰泛型函数。

运行选项介绍

使用方法

运行 cjc 编译的可执行文件 test ,添加参数选项

./test --bench --filter MyTest.*Test,-stringTest

--bench

默认情况下,只有被 @TestCase 修饰的函数会被执行。在使用 --bench 的情况下只执行 @Bench 宏修饰的用例。

--filter

如果您希望以测试类和测试用例过滤出测试的子集,可以使用 --filter=测试类名.测试用例名 的形式来筛选匹配的用例,例如:

  1. --filter=* 匹配所有测试类
  2. --filter=*.* 匹配所有测试类所有测试用例(结果和*相同)
  3. --filter=*.*Test,*.*case* 匹配所有测试类中以 Test 结尾的用例,或者所有测试类中名字中带有 case 的测试用例
  4. --filter=MyTest*.*Test,*.*case*,-*.*myTest 匹配所有 MyTest 开头测试类中以 Test 结尾的用例,或者名字中带有 case的用例,或者名字中不带有 myTest 的测试用例

另外,--filter 后有 = 和无 = 均支持。

--timeout-each=timeout

使用 --timeout-each=timeout 选项等同于对所有的测试类使用 @Timeout[timeout] 修饰。若代码中已有 @Timeout[timeout] ,则将被代码中的超时时间覆盖,即选项的超时时间配置优先级低于代码中超时时间配置。

timeout 的值应符合以下语法: number ('millis' | 's' | 'm' | 'h') 例如: 10s, 9millis 等。

  • millis : 毫秒
  • s : 秒
  • m : 分钟
  • h : 小时

--parallel

打开 --parallel 选项将使测试框架在单独的多个进程中并行执行不同的测试类。 测试类之间应该是相互独立的,不依赖于共享的可变的状态值。 程序静态初始化可能会发生多次。 不允许与 --bench 同时使用。由于性能用例对底层资源敏感,用例是否并行执行,将影响性能用例的结果,因此禁止与 --bench 同时使用。

  • --parallel=<BOOL> <BOOL> 可为 truefalse ,指定为 true 时,测试类可被并行运行,并行进程个数将受运行系统上的CPU核数控制。另外,--parallel 可省略 =true
  • --parallel=nCores 指定了并行的测试进程个数应该等于可用的 CPU 核数。
  • --parallel=NUMBER 指定了并行的测试进程个数值。该数值应该为正整数。
  • --parallel=NUMBERnCores 指定了并行的测试进程个数值为可用的 CPU 核数的指定数值倍。该数值应该为正数(支持浮点数或整数)。

--option=value

--option=value 形式提供的任何非上述的选项均按以下规则处理并转换为配置参数(类似于 @Configure 宏处理的参数),并按顺序应用:

optionvalue 为任意自定义的运行配置选项键值对,option 可以为任意通过 - 连接的英文字符,转换到 @Configure 时将转换为小驼峰格式。value 的格式规则如下:

注:当前未检查 optionvalue 的合法性,并且选项的优先级低于代码中 @Configure 的优先级。

  • 如果省略 =value 部分,则该选项被视为 Booltrue , 例如:--no-color 生成配置条目 noColor = true
  • 如果``value严格为truefalse,则该选项被视为具有相应含义的Bool 值:--no-color=false生成配置条目noColor = false` 。
  • 如果 value 是有效的十进制整数,则该选项被视为 Int64 值 , 例如:--random-seed=42 生成配置条目 randomSeed = 42
  • 如果 value 是有效的十进制小数,则该选项被视为 Float64 值 , 例如:--angle=42.0 生成配置条目 angle = 42
  • 如果 value 是带引号的字符串文字(被 " 符号包围),则该选项将被视为 String 类型,并且该值是通过解码 " 符号之间的字符串值来生成的,并处理转义符号,例如 \n \t\" 作为对应的字符值。例如,选项 --mode="ABC \"2\"" 生成配置条目 mode = "ABC \"2\""
  • 除上述情况外 value 值将被视为 String 类型,该值从所提供的选项中逐字获取。例如, --mode=ABC23[1,2,3] 生成配置条目 mode = "ABC23[1,2,3]"

package unittest

enum TimeoutInfo

public enum TimeoutInfo {
    | NoTimeout
    | Timeout(Duration)

指定测试超时的信息。目前,可以通过两种方式提供此超时信息:

  1. 在执行测试文件时打开 --timeout-each 选项。
  2. 在测试类或测试用例上使用 @Timeout[expr] 宏。

NoTimeout

没有提供超时信息。

Timeout(Duration)

指定超时时间。

func fromDefaultConfiguration

public static func fromDefaultConfiguration(): TimeoutInfo

功能:返回测试过程的 CLI 参数中提供的超时信息。

返回值:超时信息。

func applyDefault

public func applyDefault(default: TimeoutInfo): TimeoutInfo

功能:返回根据默认值更新的超时信息。

参数:

  • default :来自于 CLI 参数或测试类上的超时信息,作为默认值

返回值:超时信息。

interface Measurement

public interface Measurement {
    func measure(f: () -> Unit): Float64
    func toString(f: Float64): String
}

功能:在性能测试过程中可以收集和分析各种数据的接口。性能测试框架使用的特定实例由 @Configure 宏的 measurement 参数指定。

func measure

func measure(f: () -> Unit): Float64

功能: 返回将用于统计分析的测量数据的表示。

参数:

  • f : 是一个 lambda,应该测量它的执行信息。

返回值:测量得到的数据

func toString

func toString(f: Float64): String

功能:将测量数据的浮点表示转换为将在性能测试输出中使用的字符串。

参数:

  • f : 测量数据的浮点表示

返回值:按规则转换后的字符串

class RawBench

public class RawBencher<T> { 
    public init() 
    public init(initial: T)
    public func beforeInvocation(before: () -> T)
    public func afterInvocation(after: (T) -> Unit )
    public func runBench(bench: (T) -> Unit )
}

extend RawBencher<T> where T <: Unit { 
    public func afterInvocation(after: () -> Unit )
    public func runBench(bench: () -> Unit )
}

提供较低级别的性能测试 API ,以便对性能测试过程进行更细粒度的控制。使用时应小心,因为如果使用不当,可能会产生误导性结果。

init

public init() 

功能:默认构造函数。只应在 @RawBench 注释的性能测试用例中调用。

异常:

  • IllegalStateException : 如果不是在 @RawBench 注释的性能测试用例中创建的,则抛出IllegalStateException

init

public init(initial: T) 

功能:带初始化值的默认构造函数。只应在 @RawBench 注释的性能测试用例中调用。

参数:

  • initial : 测试用例会使用的初始化值

异常:

  • IllegalStateException : 如果不是在 @RawBench 注释的性能测试用例中创建的,则抛出IllegalStateException

func beforeInvocation

beforeInvocation(before: () -> T)

功能:提供在每次调用 runBench 之前执行并且不统计入性能测试结果的代码。 为了提供可靠的性能测试结果,before 的执行应该尽可能具有确定性,并且它应该比实际的性能测试函数花费更少的时间。如果可能的话,堆分配的数量也应该最小化。

参数:

  • before: 带有入参的被注册的函数。

异常:

  • IllegalStateException: 当在 runBench 调用后或者其内部调用时将抛出异常。

func afterInvocation

public func afterInvocation(after: (T) -> Unit)

功能:提供在每次调用 runBench 之后执行并且不统计入性能测试结果的代码。 为了提供可靠的性能测试结果,after 的执行应该尽可能具有确定性,并且它应该比实际的性能测试函数花费更少的时间。如果可能的话,堆分配的数量也应该最小化。 特别是,即使它是在 beforeInvocation 回调之后立即执行而不在其间调用 runBench ,它也应该以相同的方式工作。

参数:

  • after: 带有入参的被注册的函数。

异常:

  • IllegalStateException :当在 runBench 调用后或者其内部调用时将抛出异常。

func afterInvocation

public func afterInvocation(after: () -> Unit)

功能:提供在每次调用 runBench 之后执行并且不统计入性能测试结果的代码。 为了提供可靠的性能测试结果,after 的执行应该尽可能具有确定性,并且它应该比实际的性能测试函数花费更少的时间。如果可能的话,堆分配的数量也应该最小化。 特别是,即使它是在 beforeInvocation 回调之后立即执行而不在其间调用 runBench ,它也应该以相同的方式工作。

参数:

  • after: 无入参的被注册的函数。

异常:

  • IllegalStateException :当在 runBench 调用后或者其内部调用时将抛出异常。

func runBench

public func runBench(bench: (T) -> Unit)

功能:执行实际的性能测试流程。所有设置都继承自包含测试用例的 @Configure 。 性能测试用例的输入数据来自构造函数或前一个 beforeInvocation 调用。

参数:

  • bench: 带有入参的被注册的函数。

func runBench

public func runBench(bench: () -> Unit)

功能:执行实际的性能测试流程。所有设置都继承自包含测试用例的 @Configure 。 性能测试用例的输入数据来自构造函数或前一个 beforeInvocation 调用。

参数:

  • bench: 无入参的被注册的函数。

示例

如何对 Array.sort 进行性能测试:

@Test
class BenchSort {
    @RawBench[len in [1,100,10000]]
    func sort(len: Int64): Unit {
        let b = RawBencher<Array<Int64>>()
        b.beforeInvocation{ => Array(len,{ i: Int64 => len-i}) }
        b.runBench { x =>
            x.sort()
        }   
    }
}

请注意,上例每次都会分配新数组,这可能会导致后台进行大量 GC 工作,从而导致结果不太精确。

如何更好得对 Array.sort 进行性能测试,消除过多对象分配:

@Test
class BenchSort {
    @RawBench[len in [1,100,10000]]
    func sort(len: Int64): Unit {
        let b = RawBencher<Array<Int64>>()
        let array = Array(len,{ i: Int64 => len-i})
        b.beforeInvocation{ => array.clone() }
        b.runBench { x =>
            x.sort()
        }   
    }
}

请记住,并不总是可以在 beforeInvocation 中将对象恢复到其原始状态的同时满足 beforeInvocation 的所有要求。 例如,如下即为不正确的方式对 ArrayList.append 进行性能测试:

@Test
class BenchAppend {
    @RawBench
    func append(): Unit {
        let b = RawBencher<ArrayList<Int64>>()
        let data = ArrayList()
        b.beforeInvocation{ => 
            data.clear() 
            data
        }
        b.runBench { x =>
            x.append(1)
        }   
    }
}

上例看起来正确,但是它并不满足 beforeInvocation 的要求。 它的问题是,根据是否调用基准测试,ArrayList.clear() 的工作方式有所不同。

一种正确的,但依然不够好的对 ArrayList.append 的性能测试方案:

@Test
class BenchAppend {
    @RawBench
    func append(): Unit {
        let b = RawBencher<ArrayList<Int64>>()
        b.beforeInvocation{ => 
            ArrayList()
        }
        b.runBench { x =>
            x.append(1)
        }   
    }
}

这里的问题是 ArrayList 的创建比 ArrayList.append 花费更多的时间,因此 append 的实际时间可能会在测量噪声中丢失。

如下为一种更好的方式对 ArrayList.append 进行测试:

@Test
class BenchAppend {
    @RawBench[times in [10,100]]
    func append(times: Int64): Unit {
        let b = RawBencher<ArrayList<Int64>>()
        b.beforeInvocation{ => 
            ArrayList()
        }
        b.runBench { x =>
            for i in 0..times {
                x.append(1)
            }
        }
    }
}

这仍然不是最好的方法,因为性能测试结果将包括循环本身的执行时间。但目前在很多场景上它都比其他选择更好。

class TimeNow

public struct TimeNow <: Measurement {
    public init(unit: ?TimeUnit) 
    public init()
    public func measure(f: () -> Unit): Float64
    public func toString(duration: Float64): String
}

功能: Measurement 的实现,用于测量执行一个函数所花费的时间。

init

public init(unit: ?TimeUnit) 

功能: unit 参数用于指定打印结果时将使用的时间单位。

参数:

  • unit: 指定的时间单位

init

public init()

功能:自动选择输出格式的默认构造函数。

func measure

    public func measure(f: () -> Unit): Float64

功能:计算将用于统计分析的测量数据

参数:

  • f :被计算时间的执行体

返回值:

计算得到的数据,用于统计分析

func toString

    public func toString(duration: Float64): String

功能:按时间单位打印传入的时间值

参数:

  • duration: 需要被打印的时间数值

返回值:按指定单位输出的时间数字字符串

enum TimeUnit

public enum TimeUnit {
    | Nanos
    | Micros
    | Millis
    | Seconds
}

功能:可以在 TimeNow 类构造函数中使用的时间单位。

Nanos

Nanos

功能: 单位为纳秒

Micros

Micros

功能: 单位为微秒

Millis

Millis

功能: 单位为毫秒

Seconds

Seconds

功能: 单位为秒

enum ExplicitGcType

public enum ExplicitGcType{
    Disabled |
    Light |
    Heavy
}

功能:用于指定 @Configure 宏的 explicitGC 配置参数。表示 GC 执行的三种不同方式。

Disabled

Disabled

功能: GC不会被框架显式调用。

Light

Light

功能: std.runtime.GC(heavy: false) 将在 Benchmark 函数执行期间由框架显式调用。这是默认设置。

Heavy

Heavy

功能: std.runtime.GC(heavy: true) 将在性能测试执行期间由框架显式调用。

class JsonStrategy

public class JsonStrategy<T> <: DataStrategy<T> where T <: Serializable<T> {
    public override func provider(configuration: Configuration): SerializableProvider<T> 
}

功能:DataStrategy 对 JSON 数据格式的序列化实现

func provider

public override func provider(configuration: Configuration): SerializableProvider<T>

功能:生成序列化数据迭代器

参数:

  • configuration: 数据配置信息

返回值:序列化迭代器对象

func json

public func json<T>(fileName: String): JsonStrategy<T> where T <: Serializable<T> 

功能: 返回一个 JsonStrategy<T> 对象, T 可被序列化,数据值从 JSON 文件中读取。

参数:

  • fileName : JSON 格式的文件地址,可为相对地址

返回值:一个 JsonStrategy<T> 对象, T 可被序列化,数据值从 JSON 文件中读取。

class CsvStrategy

public class CsvStrategy<T> <: DataStrategy<T> where T <: Serializable<T> {
    public override func provider(configuration: Configuration): SerializableProvider<T>
}

功能:DataStrategy 对 CSV 数据格式的序列化实现

func provider

public override func provider(configuration: Configuration): SerializableProvider<T>

功能:生成序列化数据迭代器

参数:

  • configuration: 数据配置信息

返回值:序列化迭代器对象

func csv

public func csv<T>(
	fileName: String,
	delimiter!: Char = ',',
    quoteChar!: Char = '"',
    escapeChar!: Char = '"',
    commentChar!: Option<Char> = None,
    header!: Option<Array<String>> = None,
    skipRows!: Array<UInt64> = [],
    skipColumns!: Array<UInt64> = [],
    skipEmptyLines!: Bool = false
): CsvStrategy<T> where T <: Serializable<T>

功能: 返回一个 CsvStrategy<T> 对象, T 可被序列化,数据值从 CSV 文件中读取。

参数:

  • fileName : CSV 格式的文件地址,可为相对地址,不限制后缀名。
  • delimiter :一行中作为元素分隔符的符号。默认值为 , (逗号)。
  • quoteChar :- 括住元素的符号。默认值为 " (双引号)。
  • escapeChar :转义括住元素的符号。默认值为 " (双引号)。
  • commentChar :注释符号,跳过一行。必须在一行的最左侧。默认值是 None (不存在注释符号)。
  • header :提供一种方式覆盖第一行。
    • 当 header 被指定时,文件的第一行将被作为数据行,指定的 header 将被使用。
    • 当 header 被指定,同时第一行通过指定 skipRows 被跳过时,第一行将被忽略,指定的 header 将被使用。
    • 当 header 未被指定时,即值为 None 时,文件的第一行将被作为表头。此为默认值。
  • skipRows :指定需被跳过的数据行号,行号从 0 开始。默认值为空数组 []
  • skipColumns :指定需被跳过的数据列号,列号从 0 开始。当有数据列被跳过,并且用户指定了自定义的 header 时,该 header 将按照跳过后的实际数据列对应。默认值为空数据 []
  • skipEmptyLines :指定是否需要跳过空行。默认值为 false

返回值:CsvStrategy<T> 对象, T 可被序列化,数据值从 CSV 文件中读取。

func tsv

public func tsv<T>(
	fileName: String,
    quoteChar!: Char = '"',
    escapeChar!: Char = '"',
    commentChar!: Option<Char> = None,
    header!: Option<Array<String>> = None,
    skipRows!: Array<UInt64> = [],
    skipColumns!: Array<UInt64> = [],
    skipEmptyLines!: Bool = false
): CsvStrategy<T> where T <: Serializable<T>

功能: 返回一个 CsvStrategy<T> 对象, T 可被序列化,数据值从 TSV 文件中读取。

Parameters:

  • fileName : TSV 格式的文件地址,可为相对地址,不限制后缀名。
  • quoteChar :- 括住元素的符号。默认值为 " (双引号)。
  • escapeChar :转义括住元素的符号。默认值为 " (双引号)。
  • commentChar :注释符号,跳过一行。必须在一行的最左侧。默认值是 None (不存在注释符号)。
  • header :提供一种方式覆盖第一行。
    • 当 header 被指定时,文件的第一行将被作为数据行,指定的 header 将被使用。
    • 当 header 被指定,同时第一行通过指定 skipRows 被跳过时,第一行将被忽略,指定的 header 将被使用。
    • 当 header 未被指定时,即值为 None 时,文件的第一行(跳过后的实际数据)将被作为表头。此为默认值。
  • skipRows :指定需被跳过的数据行号,行号从 0 开始。默认值为空数组 []
  • skipColumns :指定需被跳过的数据列号,列号从 0 开始。当有数据列被跳过,并且用户指定了自定义的 header 时,该 header 将按照跳过后的实际数据列对应。默认值为空数据 []
  • skipEmptyLines :指定是否需要跳过空行。默认值为 false

返回值:CsvStrategy<T> 对象, T 可被序列化,数据值从 TSV 文件中读取。

如何使用 json 文件进行参数化测试

用例示例:

@Test[user in json("users.json")]
func test_user_age(user: User): Unit {
    @Expect(user.age, 100)
}

json 文件示例:

[
    {
        "age": 100
    },
    {
        "age": 100
    }
]

创建一种被用作测试函数参数的类,该类实现接口 Serializable

class User <: Serializable<User> {
    User(let age: Int64) {}

    public func serialize(): DataModel {
        DataModelStruct()
          .add(Field("age", DataModelInt(age)))
    }

    public static func deserialize(dm: DataModel): User {
        if (let Some(dms) <- dm as DataModelStruct) {
          if (let Some(age) <- dms.get("age") as DataModelInt) {
            return User(age.getValue())
          }
        }

        throw Exception("Can't deserialize user.")
    }
}

任何实现 Serializable 的类型都可以用作参数类型,包括默认值:

@Test[user in json("numbers.json")]
func test(value: Int64)
@Test[user in json("names.json")]
func test(name: String)

如何使用 csv/tsv 文件进行参数化测试

在单元测试中,可以通过传入 csv/tsv 文件地址进行参数化测试。

CSV 文件每一行的数据应当被表示成一个 Serializable<T> 对象,它的成员名是文件每一列头的值,成员值是 DataModelString 类型的对应列号上的值。

举例来说,有一个 testdata.csv 文件,具有如下内容:

username,age
Alex Great,21
Donald Sweet,28

有几种方式可以序列化上述数据:

  1. 将数据表示为 HashMap<String, String> 类型。

具体示例为:

from std import collection.HashMap
from std import unittest.*
from std import unittest.testmacro.*


@Test[user in csv("testdata.csv")]
func testUser(user: HashMap<String, String>) {
	@Assert(user["username"] == "Alex Great" || user["username"] == "Donald Sweet")
	@Assert(user["age"] == "21" || user["age"] == "28")
}
  1. 将数据表示为 Serializable<T> 类型数据,其 String 类型的数据可被反序列化为 DataModelStruct 格式对象。

具体示例为:

from serialization import serialization.*
from std import convert.*
from std import unittest.*
from std import unittest.testmacro.*


public class User <: Serializable<User> {
    public User(let name: String, let age: UInt32) {}

    public func serialize(): DataModel {
        let dms = DataModelStruct()
        dms.add(Field("username", DataModelString(name)))
        dms.add(Field("age", DataModelString(age.toString())))
        return dms
    }

    static public func deserialize(dm: DataModel): User {
        var data: DataModelStruct = match (dm) {
            case dms: DataModelStruct => dms
            case _ => throw DataModelException("this data is not DataModelStruct")
        }

        let name = String.deserialize(data.get("username"))
        let age = String.deserialize(data.get("age"))
        return User(name, UInt32.parse(age))
    }
}

@Test[user in csv("testdata.csv")]
func testUser(user: User) {
    @Assert(user.name == "Alex Great" || user.name == "Donald Sweet")
    @Assert(user.age == 21 || user.age == 28)
}

Re-exported declarations

unittest 重导出了下列类型:

public import unittest.common.checkDataStrategy
public from std import unittest.common.Configuration
public from std import unittest.common.ConfigurationKey
public from std import unittest.common.DataProvider
public from std import unittest.common.DataStrategy
public from std import unittest.common.DataShrinker
public from std import unittest.common.DataFinisher
public import unittest.prop_test.random
public import unittest.prop_test.Arbitrary
public import unittest.prop_test.Shrink

package unittest.testmacro

macro Bench

public macro Bench(input: Tokens): Tokens

功能:@Bench 宏的实现, @Bench 可用于在 @Test 宏内声明的函数。可以使用 @Configure 宏来配置 @Bench 宏的行为。

参数:

  • input :@Bench 修饰的函数的 Tokens

返回值:经过处理后的 Tokens ,该用例将作为性能测试用例

异常 MacroException - 如果 input 不是 FuncDecl ,抛出异常

macro Bench

public macro Bench(dslArguments: Tokens, input: Tokens): Tokens

功能: @Bench 宏的实现, @Bench 可用于在 @Test 宏内声明的函数。可以使用 @Configure 宏来配置 @Bench 宏的行为。

参数:

  • input :@Bench 修饰的 Tokens
  • dslArguments - 参数的配置 DSL

返回值:经过处理后的 Tokens ,该用例将作为性能测试用例

异常 MacroException : 如果 input 不是 FuncDecl ,抛出异常

macro RawBench

public macro RawBench(input: Tokens): Tokens

功能: @RawBench 宏的实现, @RawBench 可用于在 @Test 宏内声明的函数。可以使用 @Configure 宏来配置 @RawBench 宏的行为。提供比 @Bench 宏更细粒度的 API 。

参数:

  • input :@RawBench 修饰的 Tokens

返回值:经过处理后的 Tokens ,该用例将作为性能测试用例

异常 MacroException : 如果 input 不是 FuncDecl ,抛出异常

macro RawBench

public macro RawBench(dslArguments: Tokens, input: Tokens): Tokens

功能: @RawBench 宏的实现, @RawBench 可用于在 @Test 宏内声明的函数。可以使用 @Configure 宏来配置 @RawBench 宏的行为。提供比 @Bench 宏更细粒度的 API 。

参数:

  • input :@RawBench 修饰的 Tokens
  • dslArguments - 参数的配置 DSL

返回值:经过处理后的 Tokens ,该用例将作为性能测试用例

异常 MacroException : 如果 input 不是 FuncDecl ,抛出异常

macro Expect

public macro Expect(input: Tokens): Tokens

@Expect 宏的实现, @Testcase 宏声明的函数内部使用, 用于断言。@Expect(expr1, expr2) 接受两个表达式,左边是实际执行的值,右边是期望的值。@Expect(condition: Bool) 比较 condition 是否为 true 。比较左右两边是否相同,与 @Assert 不同的是断言失败继续执行用例。

参数:

  • input :@Expect 修饰的 Tokens

返回值:经过处理后的 Tokens ,用于判断测试中的变量是否符合预期

macro Assert

public macro Assert(input: Tokens): Tokens

@Assert 宏的实现, @Testcase 宏声明的函数内部使用,用于断言。@Assert(expr1, expr2) 接受两个表达式,左边是实际执行的值,右边是期望的值。@Assert(condition: Bool) 比较 condition 是否为 true 。比较左右两边是否相同,与 @Expect 不同的是断言失败停止用例。

参数:

  • input :@Assert 修饰的 Tokens

返回值:经过处理后的 Tokens ,用于判断测试中的变量是否符合预期

macro PowerAssert

public macro PowerAssert(input: Tokens): Tokens

功能:@PowerAssert(condition: Bool) 检查传递的表达式是否为真,并显示包含传递表达式的中间值和异常的详细图表。 请注意,现在并非所有 AST 节点都受支持。支持的节点如下:

  • 任何二进制表达式
    • 算术表达式,如a + b == p % b
    • 布尔表达式,如 a || b == a && b
    • 位表达式,如a | b == a ^ b
  • 成员访问如 a.b.c == foo.bar
  • 括号化的表达式,如 (foo) == ((bar))
  • 调用表达式,如 foo(bar()) == Zoo()
  • 引用表达式,如 x == y
  • 赋值表达式,如a = foo,实际上总是 Unit (表示为 ()),请注意,赋值表达式的左值不支持打印
  • 一元表达式,如 !myBool
  • is 表达式,如 myExpr is Foo
  • as 表达式,如 myExpr as Foo 如果传递了其他节点,则图中不会打印它们的值 返回的 Tokens 是初始表达式,但包装到一些内部包装器中,这些包装器允许进一步打印中间值和异常。

参数:

  • input :@Assert 修饰的 Tokens

返回值:经过处理后的 Tokens ,初始表达式,但包装到一些内部包装器中,这些包装器允许进一步打印中间值和异常。

macro Skip

public macro Skip(input: Tokens): Tokens

功能:@Skip 宏的实现,只在用于测试类内使用 @Testcase 宏声明的函数,@Skip 修饰使用 @Testcase 宏声明的函数后,执行测试会跳过该用例。

参数:

  • input :@Skip 修饰的 Tokens

返回值:经过处理后的 Tokens,用于跳过该测试用例

异常 MacroException : 如果 input 不是 ClassDecl 或者 FuncDecl ,抛出异常

macro Skip

public macro Skip(attr: Tokens, input: Tokens): Tokens

功能:@Skip 宏的实现,只在用于测试类内使用 @Testcase 宏声明的函数,@Skip 修饰使用 @Testcase 宏声明的函数后,执行测试会跳过该用例。

参数:

  • input :@Skip 修饰的 Tokens
  • attr - @Skip[attr] 中的 attr ,只支持为 true ,其他参数用例不跳过。

返回值:经过处理后的 Tokens,用于跳过该测试用例

异常 MacroException : 如果 input 不是 ClassDecl 或者 FuncDecl ,抛出异常

macro Timeout

public macro Timeout(attr: Tokens, input: Tokens): Tokens

@Timeout 宏可以与 @TestCase@Test 。指定超时值。

参数:

  • attr : 类型为 std.time.Duration 的表达式,作为用例执行的超时时间
  • input : @Timeout 修饰的 Tokens

返回值:经过处理后的 Token

异常 MacroException : 如果 input 不是 ClassDecl 或者 FuncDecl ,抛出异常

macro Parallel

public macro Parallel(input: Tokens): Tokens

@Parallel 宏可以与 @Test 共同使用,指定该用例类中所有用例可并行执行。

参数:

  • input : @Parallel 修饰的 Tokens

返回值:经过处理后的 Token

macro TestCase

public macro TestCase(input: Tokens): Tokens

功能: @Testcase 宏的实现,只在 @Test 修饰的 class 内部生效, 修饰非测试类成员函数时无效。@Testcase 修饰的函数必须为返回值类型为 Unit 的函数

参数:

  • input :@Testcase 修饰的 Tokens

返回值:经过处理后的 Tokens ,用于将测试类中的函数变为测试用例

异常 MacroException : 如果 input 不是 FuncDecl ,抛出异常

macro TestCase

public macro TestCase(dslArguments: Tokens, input: Tokens): Tokens

功能: @Testcase 宏的实现,只在 @Test 修饰的 class 内部生效, 修饰非测试类成员函数时无效。@Testcase 修饰的函数必须为返回值类型为 Unit 的函数

参数:

  • input :@Testcase 修饰的 Tokens
  • dslArguments - 参数的配置 DSL

返回值:经过处理后的 Tokens ,用于将测试类中的函数变为测试用例

异常 MacroException : 如果 input 不是 FuncDecl ,抛出异常

macro Test

public macro Test(input: Tokens): Tokens

功能: @Test 宏的实现,修饰 Top-Level 类或者 Top-Level 的函数(此时也会宏展开为测试类)。 @Test 修饰的类不能是泛型类且必须有无参构造函数。 @Test 修饰的 Top-Level 的函数必须为返回值类型为 Unit 的函数

参数:

  • input : @Test 修饰的 Tokens

返回值:经过处理后的 Tokens ,用于将修饰的类和函数变为测试类

异常:

  • MacroException : 如果 input 不是 ClassDecl 或 FuncDecl ,抛出异常
  • IllegalArgumentException - 如果输入是 ClassDecl ,并且在类体内中找到了 PrimaryCtorDecl ,并且 PrimaryCtorDecl 体内为空,抛出异常

macro Test

public macro Test(dslArguments: Tokens, input: Tokens): Tokens

功能: @Test 宏的实现,修饰 Top-Level 类或者 Top-Level 的函数(此时也会宏展开为测试类)。 @Test 修饰的类不能是泛型类且必须有无参构造函数。 @Test 修饰的 Top-Level 的函数必须为返回值类型为 Unit 的函数

参数:

  • input : @Test 修饰的 Tokens
  • dslArguments - 参数的配置 DSL

返回值:经过处理后的 Tokens ,用于将修饰的类和函数变为测试类

异常:

  • MacroException : 如果 input 不是 ClassDecl 或 FuncDecl ,或者 classDecl 的无携带参数的构造函数,抛出异常

macro Configure

public macro Configure(dslArguments: Tokens, input: Tokens): Tokens

功能: @Configure@Test@TestCase@Bench 宏一起放置在声明上。如果与 @Test 宏一起使用,必须放在 @Test 宏之后。用于设置测试用例的配置参数。 语法:@Configure(parameter1:value1,parameter2:value2)@Configure 宏仅用于设置内部配置参数,在单元测试框架中的使用方式请参阅各组件文档。

参数:

  • input : @Configure 修饰的 Tokens
  • dslArguments - 参数的配置 DSL

返回值:通常返回 input 相同的值。

异常:

  • MacroException : 如果 dslArguments 格式不正确,则抛出异常。

macro Types

public macro Types(dslArguments: Tokens, input: Tokens): Tokens

功能:@Types@Test@TestCase@Bench 宏一起放置在声明上。用于设置带泛型参数的测试用例的类型参数。 语法:@Types[T in <Int64, String, Array<Int64>>, U in <SomeClass, SomeClass2>].

更多信息,详见 《带类型参数的参数化测试》章节。

参数:

  • input : @Types 修饰的 Tokens
  • dslArguments - 类型参数 DSL

返回值:返回使用传入的类型参数替换泛型参数的类或函数声明 Tokens 。

异常:

  • MacroException : 如果 dslArguments 格式不正确,则抛出异常。

package unittest.common

class Configuration

public class Configuration <: ToString
    public init()
    public func get<T>(key: String): ?T
    public func set<T>(key: String, value: T): Unit
    public func remove<T>(key: String): ?T
    public func clone(): Configuration
    public func toString(): String
}

存储 @Configure 宏生成的 unittest 配置数据的对象。Configuration 是一个类似 HashMap 的类,但它的键不是键和值类型,而是 String 类型,和任何给定类型的值

init

public init()

功能:构造一个空的实例。

func get

public func get<T>(key: String): ?T

功能:获取 key 对应的值。

参数:

  • key : 键名称
  • T : 泛型参数,用于在对象中查找对应类型的值

返回值:未找到时返回 None ,找到对应类型及名称的值时返回 Some() 。

func set

public func set<T>(key: String, value: T): Unit

功能:给对应键名称和类型设置值。

参数:

  • key :键名称
  • T :类型名称

func remove

public func remove<T>(key: String): ?T

功能:删除对应键名称和类型的值。

参数:

  • key :键名称
  • T :类型名称

返回值:当存在该值时返回该值,当不存在时返回 None 。

func clone

public func clone(): Configuration

功能:拷贝一份对象

返回值:拷贝的对象

func toString

public func toString(): String

功能:该对象的字符化对象,当内部对象未实现 ToString 接口时,输出 '' 。

返回值:字符串

interface DataProvider

public interface DataProvider<T> {
    func provide(): Iterable<T>
    prop isInfinite: Bool
}

DataStrategy 的组件,用于提供测试数据, T 指定提供者提供的数据类型。

func provide()

func provide(): Iterable<T>

功能:获取数据迭代器。

返回值:数据迭代器

prop isInfinite

prop isInfinite: Bool

功能:是否无法穷尽。

interface DataShrinker

public interface DataShrinker<T> {
    func shrink(value: T): Iterable<T>
}

DataStrategy 的组件,用于在测试期间缩减数据,T 指定该收缩器处理的数据类型。

func shrink

func shrink(value: T): Iterable<T>

功能:获取类型 T 的值并生成较小值的集合。什么被认为是“较小”取决于数据的类型。

参数:

  • value : 被缩减的值

返回值:较小值的集合,当数据无法再被缩减时返回空集合。

interface DataFinisher

public interface DataFinisher<T> {
    func finish(value: T): Unit
}

DataStrategy 的组件,用于在测试完成后对数据进行后处理。

func finish

func finish(value: T): Unit

功能:对值进行后处理步骤

参数:

  • value : 被处理的值

interface DataStrategy

public interface DataStrategy<T> {
    func provider(configuration: Configuration): DataProvider<T>
    func shrinker(configuration: Configuration): DataShrinker<T>
    func finisher(configuration: Configuration): DataFinisher<T>
}

为参数化测试提供数据的策略,T 指定该策略操作的数据类型。

func provider

func provider(configuration: Configuration): DataProvider<T>

功能:获取提供测试数据组件。

参数:

  • configuration : 配置信息

返回值:提供测试数据的组件对象。

func shrinker

func shrinker(configuration: Configuration): DataShrinker<T>

功能:获取缩减测试数据的组件。

参数:

  • configuration : 配置信息

返回值:缩减测试数据的组件对象。

func finisher

func finisher(configuration: Configuration): DataFinisher<T>

功能:获取对测试数据进行测试后操作的组件。

参数:

  • configuration : 配置信息

返回值:对测试数据进行测试后操作测试数据的组件对象。

extend Array

extend Array<T> <: DataStrategy<T> & DataProvider<T>

为 Array 实现了 DataStrategy 和 DataProvider 接口。 使如下配置形式可用:

@Test[x in [1,2,3]]
func test(x: Int64) {}

extend Range

extend Range<T> <: DataStrategy<T> & DataProvider<T>

为 Range 实现了 DataStrategy 和 DataProvider 接口。 使如下配置形式可用:

@Test[x in (0..5)]
func test(x: Int64) {}

package unittest.prop_test

interface Generator

public interface Generator<T> {
    func next(): T
}

生成器生成 T 类型的值。

func next

func next(): T

功能:获取生成出来的 T 类型的值。

返回值:生成的 T 类型的值

interface Arbitrary

public interface Arbitrary<T> {
    static func arbitrary(random: Random): Generator<T>
}

生成 T 类型随机值的接口

func arbitrary

static func arbitrary(random: Random): Generator<T>

功能:获取生成 T 类型随机值生成器。

参数:

  • random :随机数

返回值:生成 T 类型随机值生成器。

extend Types by Arbitrary

extend Unit <: Arbitrary<Unit> {}
extend Bool <: Arbitrary<Bool> {}
extend UInt8 <: Arbitrary<UInt8> {}
extend UInt16 <: Arbitrary<UInt16> {}
extend UInt32 <: Arbitrary<UInt32> {}
extend UInt64 <: Arbitrary<UInt64> {}
extend Int8 <: Arbitrary<Int8> {}
extend Int16 <: Arbitrary<Int16> {}
extend Int32 <: Arbitrary<Int32> {}
extend Int64 <: Arbitrary<Int64> {}
extend IntNative <: Arbitrary<IntNative> {}
extend UIntNative <: Arbitrary<UIntNative> {}
extend Float16 <: Arbitrary<Float16> {}
extend Float32 <: Arbitrary<Float32> {}
extend Float64 <: Arbitrary<Float64> {}
extend Ordering <: Arbitrary<Ordering> {}
extend Char <: Arbitrary<Char> {}
extend String <: Arbitrary<String> {}
extend Array<T> <: Arbitrary<Array<T>> where T <: Arbitrary<T> {}
extend Option<T> <: Arbitrary<Option<T>> where T <: Arbitrary<T> {}
extend Ordering <: Arbitrary<Ordering> {}

interface Shrink

public interface Shrink<T> {
    func shrink(): Iterable<T>
}

将 T 类型的值缩减到多个“更小的”值

func shrink

func shrink(): Iterable<T>

功能:将该值缩小为一组可能的“较小”值

返回值:一组可能的“较小”值的迭代器

extend Types by Shrink

extend Unit <: Shrink<Unit> {}
extend Bool <: Shrink<Bool> {}
extend UInt8 <: Shrink<UInt8> {}
extend UInt16 <: Shrink<UInt16> {}
extend UInt32 <: Shrink<UInt32> {}
extend UInt64 <: Shrink<UInt64> {}
extend UIntNative <: Shrink<UIntNative> {}
extend Int8 <: Shrink<Int8> {}
extend Int16 <: Shrink<Int16> {}
extend Int32 <: Shrink<Int32> {}
extend Int64 <: Shrink<Int64> {}
extend IntNative <: Shrink<IntNative> {}
extend Float16 <: Shrink<Float16> {}
extend Float32 <: Shrink<Float32> {}
extend Float64 <: Shrink<Float64> {}
extend Ordering <: Shrink<Ordering> {}
extend Array<T> <: Shrink<Array<T>> where T <: Shrink<T> {}
extend Option<T> <: Shrink<Option<T>> where T <: Shrink<T> {}
extend Char <: Shrink<Char> {}
extend String <: Shrink<String> {}

class RandomDataProvider

public class RandomDataProvider<T> <: DataProvider<T> where T <: Arbitrary<T> {
    public init(configuration:Configuration)
    public override func provide(): Iterable<T>
    public override prop isInfinite: Bool
}

使用随机数据生成的 DataProvider 接口的实现。

init

public init(configuration:Configuration)

功能:构造一个随机数据提供者的对象。

参数:

  • configuration :配置对象,必须包含一个随机生成器,名称为 random ,类型为 random.Random

异常 IllegalArgumentException:当 configuration 不包含 random 实例时,抛出异常。

func provide

public override func provide(): Iterable<T>

功能:提供随机化生成的数据。

返回值:从 T 的任意实例创建的无限迭代器

prop isInfinite

public override prop isInfinite: Bool

功能:是否生成无限的数据。

返回值:始终返回 true

class RandomDataShrinker

public class RandomDataShrinker<T> <: DataShrinker<T> {
    public override func shrink(value: T): Iterable<T>
}

使用随机数据生成的 DataShrinker 接口的实现。

func shrinker

public override func shrink(value: T): Iterable<T>

功能:获取值的缩减器。

参数:

  • value:参数值

返回值: 如果参数实现了 Shrink 接口,则返回缩减后的迭代器,如果未实现,则返回空的数组。

class RandomDataStrategy

public class RandomDataStrategy<T> <: DataStrategy<T> where T <: Arbitrary<T> {
    public override func provider(configuration: Configuration): RandomDataProvider<T>
    public override func shrinker(_: Configuration): RandomDataShrinker<T>
}

使用随机数据生成的 DataStrategy 接口的实现。

func provider

public override func provider(configuration: Configuration): RandomDataProvider<T>

功能:获取随机数据的提供者。

参数:

  • configuration:参数配置信息

返回值: RandomDataProvider 的实例

func shrinker

public override func shrinker(_: Configuration): RandomDataShrinker<T>

功能:获取随机数据的缩减器。

参数:

  • configuration:参数配置信息

返回值: RandomDataShrinker 的实例

func random

public func random<T>(): RandomDataStrategy<T> where T <: Arbitrary<T>

功能:使用随机数据生成的 RandomDataStrategy 接口的实现。

返回值:使用随机数据生成的 RandomDataStrategy 接口的实例

示例

简单示例

// Function under test
func concat(s1: String, s2: String): String {
    return s1 + s2
}

// Test itself
@Test
class Tests {
    @TestCase
    func testConcat(): Unit {
        //期望 concat("1", "2") 运行结果为 "12"
        @Assert(concat("1", "2"), "12")
    }
}

执行命令

cjc --test test.cj && ./main

输出内容:

TCS: TestCCC, time elapsed: 0 ns, RESULT:
[ PASSED ] CASE: sayhi (0 ns)
--------------------------------------------------------------------------------------------------
Summary: TOTAL: 1
    PASSED: 1, SKIPPED: 0, ERROR: 0
    FAILED: 0

使用 TopLevel 函数:

// Function under test
func concat(s1: String, s2: String): String {
    return s1 + s2
}

// Test itself
@Test
func testConcat(): Unit {
    //期望 concat("1", "2") 运行结果为 "12"
    @Assert(concat("1", "2"), "12")
}

执行命令

cjc --test test.cj && ./main

输出内容:

TCS: TestCase_sayhi, time elapsed: 0 ns, RESULT:
[ PASSED ] CASE: sayhi (0 ns)
--------------------------------------------------------------------------------------------------
Summary: TOTAL: 1
    PASSED: 1, SKIPPED: 0, ERROR: 0
    FAILED: 0

@Test 修饰类的使用

@Test 修饰类,展开的测试类使用 @Bench@TestCase@Skip 的功能。

代码如下:

@Test
class MySimpleTest {

    //无 @TestCase 修饰非测试函数
    func test1(): Unit {
        println("hi1")
    }

    // TestCase 跟 Bench 可以同时修饰一个函数。此时函数既可以为单个测试用例,也可以作为 Benchmark 被多次执行,通过 --bench 打开 Bench 执行模式
    @Bench
    @TestCase
    //跳过
    @Skip
    func test2(): Unit {
        println("hi2")
    }

    // 最简单的 Benchmark 用例函数,输出 Benchmark 测试结果数据。
    @Bench
    //不跳过
    @Skip[false]
    func test3(): Unit {
        println("hi3")
    }

    @TestCase
    func test4(): Unit {
        println("hi4")
    }
}

运行结果如下:

hi3
hi3
hi3
hi3
hi3
...
hi3
hi3
hi3
hi3
hi4
TCS: MySimpleTest, time elapsed: 6580329566 ns, RESULT:
| Case  |    Median |        Err |    Err% |
|------ |----------:|-----------:|--------:|
| test3 |  30.23 us |  ±5.148 us |  ±17.0% |
[ PASSED ] CASE: test4 (50300 ns)
--------------------------------------------------------------------------------------------------
Summary: TOTAL: 4
    PASSED: 2, SKIPPED: 2, ERROR: 0
    FAILED: 0

自定义逻辑函数的使用

用户可以通过重写 afterAllafterEachbeforeAllbeforeEach 在测试函数调用前后处理自定义的逻辑。

代码如下:

@Test
class MySimpleTest {

    @TestCase
    func test1(): Unit {
        println("test1")
    }

    @TestCase
    func test2(): Unit {
        println("test2")
    }

    @TestCase
    func test3(): Unit {
        println("test3")
    }

    public override func afterAll(): Unit {
        println("afterAll")
    }

    public override func afterEach(): Unit {
         println("afterEach")
    }

    public override func beforeAll(): Unit {
         println("beforeAll")
    }

    public override func beforeEach(): Unit {
         println("beforeEach")
    }

}

运行结果如下:

beforeAll
beforeEach
ccc1
afterEach
beforeEach
ccc2
afterEach
beforeEach
ccc3
afterEach
afterAll
TCS: MySimpleTest, time elapsed: 0 ns, RESULT:
[ PASSED ] CASE: ccc1 (0 ns)
[ PASSED ] CASE: ccc2 (0 ns)
[ PASSED ] CASE: ccc3 (0 ns)
--------------------------------------------------------------------------------------------------
Summary: TOTAL: 3
    PASSED: 3, SKIPPED: 0, ERROR: 0
    FAILED: 0

@PowerAssert 示例

@Test 修饰类,展开的测试类使用 @TestCase 的功能以及 @PowerAssert 的断言用法。

代码如下:

func foo(x: Int64, y!: String = "foo") { y.size / x }
func bar(_: Int64) { 33 }

@Test
class TestA {
    @TestCase
    func case1(): Unit {
        let s = "123"
        let a = 1
        @PowerAssert(foo(10, y: "test" + s) == foo(s.size, y: s) + bar(a))
    }
}

运行结果如下:

TCS: TestA_case1, time elapsed: 176000000 ns, RESULT:
[ FAILED ] CASE: case1 (175000000 ns)
REASON: `foo(10, y: test   + s) == foo(s.size, y: s) + bar(a)` has been evaluated to false
Assert Failed: `(foo(10, y: test   + s) == foo(s.size, y: s) + bar(a))`
                |          |        |_||  |   |_|    |   |_|| |   |_||
                |          |       "123"  |  "123"   |  "123" |    1 |
                |          |__________||  |   |______|      | |______|
                |            "test123" |  |       3         |    33  |
                |______________________|  |_________________|        |
                            0             |        1                 |
                                          |__________________________|
                                                       34

--------------------------------------------------------------------------------------------------
Summary: TOTAL: 1
    PASSED: 0, SKIPPED: 0, ERROR: 0
    FAILED: 1, listed below:
        TCS: TestA_case1, CASE: case1

参数化测试简单示例

想象一下,您有一个 Stringreverse 实现,并且您想在许多不同的字符串上测试它。 您不需要为此编写单独的测试,您可以使用参数化测试 DSL 来代替:

func reverse(s: String) { 
   let temp = s.toRuneArray()
   temp.reverse()
   return String(temp)
}

@Test
func testA(): Unit {
    @Expect("cba", reverse("abc"))
}

@Test[s in ["ab", "bc", "Hello", ""]]
func testB(s: String): Unit {
    @Expect(s != reverse(s))
}

输出如下:

--------------------------------------------------------------------------------------------------
TP: default, time elapsed: 250236 ns, Result:
    TCS: TestCase_testA, time elapsed: 65077 ns, RESULT:
    [ PASSED ] CASE: testA (51004 ns)
    TCS: TestCase_testB, time elapsed: 71557 ns, RESULT:
    [ FAILED ] CASE: testB (45465 ns)
    REASON: After 4 generation steps:
        s = 
    with randomSeed = 1706690422527775114

    Expect Failed: `(s != reverse ( s ) == true)`
       left: false
      right: true

Summary: TOTAL: 2
    PASSED: 1, SKIPPED: 0, ERROR: 0
    FAILED: 1, listed below:
            TCS: TestCase_testB, CASE: testB
--------------------------------------------------------------------------------------------------

可以使用 random() 生成器来测试函数在随机值下的行为:

func reverse(s: String) { 
   let temp = s.toRuneArray()
   temp.reverse()
   return String(temp)
}
@Test[s in random()]
func testC(s: String): Unit {
    @Expect(s != reverse(s))
}

输出如下:

--------------------------------------------------------------------------------------------------
TP: default, time elapsed: 170350 ns, Result:
    TCS: TestCase_testC, time elapsed: 166996 ns, RESULT:
    [ FAILED ] CASE: testC (160175 ns)
    REASON: After 3 generation steps and 1 reduction steps:
    s =
    with randomSeed = 1691464803229918621

    Expect Failed: `(s != reverse ( s ) == true)`
       left: false
      right: true

Summary: TOTAL: 1
    PASSED: 0, SKIPPED: 0, ERROR: 0
    FAILED: 1, listed below:
        TCS: TestCase_testC, CASE: testC
--------------------------------------------------------------------------------------------------

请注意,由于随机生成的性质,每次运行测试时随机测试可能会给出不同的结果。 如果你想获得更稳定、可重复的结果,强烈建议配置随机生成的种子:

func reverse(s: String) { 
   let temp = s.toRuneArray()
   temp.reverse()
   return String(temp)
}
@Test[s in random()]
@Configure[randomSeed: 1] // randomSeed is set to 1
func testC(s: String): Unit {
    @Expect(s != reverse(s))
}

如果配置了随机种子,随机生成器将在每次运行测试时生成相同的值:

--------------------------------------------------------------------------------------------------
TP: default, time elapsed: 297568 ns, Result:
    TCS: TestCase_testC, time elapsed: 293383 ns, RESULT:
    [ FAILED ] CASE: testC (284963 ns)
    REASON: After 4 generation steps and 1 reduction steps:
    s =
    with randomSeed = 1

    Expect Failed: `(s != reverse ( s ) == true)`
       left: false
      right: true

Summary: TOTAL: 1
    PASSED: 0, SKIPPED: 0, ERROR: 0
    FAILED: 1, listed below:
        TCS: TestCase_testC, CASE: testC
--------------------------------------------------------------------------------------------------

请注意,输出包含运行测试时使用的 randomSeed 值。如果您将此值作为 randomSeed 参数值放入测试代码中,它应该产生完全相同的结果。

带多个参数的参数化测试

如果测试函数包含多个参数,则所有参数都可以通过参数化测试 DSL 提供。当前支持的最大参数数量为 5。

@Test[a in ["Hello", "a", ""], b in (0..4)]
func testB(a: String, b: Int64): Unit {
    println("${a}, ${b}")
    @Expect(a.size != b)
}

测试框架应用此值的不同组合来检查给定条件:

Hello, 0
Hello, 1
Hello, 2
Hello, 3
a, 0
a, 1
a, 2
a, 3
, 0
, 1
, 2
, 3
--------------------------------------------------------------------------------------------------
TP: default, time elapsed: 334280 ns, Result:
    TCS: TestCase_testB, time elapsed: 236204 ns, RESULT:
    [ FAILED ] CASE: testB (142534 ns)
    REASON: After 6 generation steps:
        a = a
        b = 1
    with randomSeed = 1706691095691478341

    Expect Failed: `(a . size != b == true)`
       left: false
      right: true

    [ FAILED ] CASE: testB (188789 ns)
    REASON: After 9 generation steps:
        a = 
        b = 0
    with randomSeed = 1706691095691478341

    Expect Failed: `(a . size != b == true)`
       left: false
      right: true

Summary: TOTAL: 1
    PASSED: 0, SKIPPED: 0, ERROR: 0
    FAILED: 1, listed below:
            TCS: TestCase_testB, CASE: testB
--------------------------------------------------------------------------------------------------

这也可以与随机生成的值一起使用:

@Test[a in ["Hello", "a", ""], b in random()]
@Configure[randomSeed: 0]
func testB(a: String, b: Int64): Unit {
    println("${a}, ${b}")
    @Expect(a.size != b)
}

输出结果为:

Hello, -144895307711186549
a, 1
a, 0
--------------------------------------------------------------------------------------------------
TP: default, time elapsed: 118190 ns, Result:
    TCS: TestCase_testB, time elapsed: 114702 ns, RESULT:
    [ FAILED ] CASE: testB (108172 ns)
    REASON: After 2 generation steps and 1 reduction steps:
    a = a
    b = 1
    with randomSeed = 0

    Expect Failed: `(a . size != b == true)`
       left: false
      right: true

Summary: TOTAL: 1
    PASSED: 0, SKIPPED: 0, ERROR: 0
    FAILED: 1, listed below:
        TCS: TestCase_testB, CASE: testB
--------------------------------------------------------------------------------------------------

如果需要,所有参数都可以随机化(在本例中我们不执行 println ,因为我们不建议将随机字符串打印到标准输出,它可能会破坏您的终端):

@Test[a in random(), b in random()]
@Configure[randomSeed: 0]
func testB(a: String, b: Int64): Unit {
    @Expect(a.size != b)
}

输出如下:

--------------------------------------------------------------------------------------------------
TP: default, time elapsed: 3226316 ns, Result:
    TCS: TestCase_testB, time elapsed: 3222958 ns, RESULT:
    [ FAILED ] CASE: testB (3214676 ns)
    REASON: After 53 generation steps and 2 reduction steps:
    a = _
    b = 1
    with randomSeed = 0

    Expect Failed: `(a . size != b == true)`
       left: false
      right: true

Summary: TOTAL: 1
    PASSED: 0, SKIPPED: 0, ERROR: 0
    FAILED: 1, listed below:
        TCS: TestCase_testB, CASE: testB
--------------------------------------------------------------------------------------------------

带类型参数的参数化测试

如果您需要在不同类型上运行测试类或函数,您可以使用类型参数进行测试。 这些测试与普通仓颉泛型类型和函数类似:您只需将测试类或测试函数设为泛型类或函数,然后使用特殊的 @Types 宏列出要运行测试的类型。

from std import collection.*
@Test
@Types[T in <String, Char, Int64>]
class HashSetTest<T> where T <: Hashable & Equatable<T> {
    @TestCase
    func testFoo(): Unit {
        @Expect(HashSet<T>().size, 0)
    }
}

当与 --test 编译器选项一起使用时,此测试的工作方式与三个对应声明为 HashSetTest<String>HashSetTest<Char>HashSetTest<Int64> 类型的测试类相同:。示例输出如下所示:

--------------------------------------------------------------------------------------------------
TP: default, time elapsed: 33645 ns, Result:
    TCS: HashSetTest<String>, time elapsed: 15947 ns, RESULT:
    [ PASSED ] CASE: testFoo (9520 ns)
    TCS: HashSetTest<Char>, time elapsed: 7644 ns, RESULT:
    [ PASSED ] CASE: testFoo (5852 ns)
    TCS: HashSetTest<Int64>, time elapsed: 6175 ns, RESULT:
    [ PASSED ] CASE: testFoo (2570 ns)
    Summary: TOTAL: 3
    PASSED: 3, SKIPPED: 0, ERROR: 0
    FAILED: 0
--------------------------------------------------------------------------------------------------

相同的机制可以应用于测试函数:

from std import collection.*
@Test
class HashSetTest {
    @TestCase
    @Types[T in <String, Char, Int64>]
    func testFoo<T>(): Unit where T <: Hashable & Equatable<T> {
        @Expect(HashSet<T>().size, 0)
    }
}

当与 --test 编译器选项一起使用时,此测试将像测试类包含三个测试函数 testFoo<String>testFoo<Char>testFoo<Int64> 一样工作。示例输出如下所示:

--------------------------------------------------------------------------------------------------
TP: default, time elapsed: 30664 ns, Result:
    TCS: HashSetTest, time elapsed: 27987 ns, RESULT:
    [ PASSED ] CASE: testFoo<String> (13846 ns)
    [ PASSED ] CASE: testFoo<Char> (3044 ns)
    [ PASSED ] CASE: testFoo<Int64> (4612 ns)
    Summary: TOTAL: 3
    PASSED: 3, SKIPPED: 0, ERROR: 0
    FAILED: 0
--------------------------------------------------------------------------------------------------

带超时信息的测试用例

from std import time.*
@Test
@Timeout[Duration.second]
func test(): Unit {
    println("Hello from test")
    while (true) {}
}

测试用例进入无限循环,但框架在一秒钟后中断它并打印报告:

Hello from test
--------------------------------------------------------------------------------------------------
TP: default, time elapsed: 1005261753 ns, Result:
    TCS: TestCase_test, time elapsed: 1005255775 ns, RESULT:
    [ FAILED ] CASE: test (1005191011 ns)
    REASON: Wait timeout, process not exit.
Summary: TOTAL: 1
    PASSED: 0, SKIPPED: 0, ERROR: 0
    FAILED: 1, listed below:
            TCS: TestCase_test, CASE: test
--------------------------------------------------------------------------------------------------

如果没有 @Timeout ,测试过程本身就会陷入无限循环。

高级特性

基于属性的随机化测试(实验特性)

基于属性的随机化测试是仓颉单元测试框架的一项实验性功能,允许您使用随机生成的值编写测试。 为了使用基于属性的随机化测试,您需要使用 random() 函数作为参数化值生成器:

@Test
class RandomTests {
    @TestCase[p in random()]
    func sayhi2(p: Int64): Unit {
        @Assert(p > 0)
    }
}

@Test[v in random()]
func ttt(v: String) {
    @Assert(v[0] != v[1])
}

在这种模式下,相应的函数参数必须全部实现接口 ArbitraryShrink (请参阅​​ unittest.prop_test 文档部分)。当运行这样的测试时,这些参数是随机生成的,并自动缩减到较小的值。

基于属性的随机测试对于根据大量随机数据快速测试代码非常有用,但您需要小心,因为生成/缩减过程是随机的,每次都可能会得到不同的结果。

随机数据生成使用以下配置参数:

  • generationSteps: Int64 用于随机生成值的步数(默认为 200)
  • reductionSteps: Int64 用于缩减值的步数(默认为 200)
  • randomSeed: Int64 框架内使用的随机值生成器的种子值。

增加 generationSteps 和, 或 reductionSteps 的值将提高生成的质量和,或缩减测试过程中使用的值,但也可能会增加测试的运行时间。建议谨慎。 randomSeed 可以显式设置此参数以使测试可重现,使用相同此参数的相同测试保证每次运行时都会产生相同的结果,如果不提供此参数,结果可能每次都会改变。

随机数生成支持自定义类型

要使用 random() 使自定义类型可用于随机生成,需要执行以下步骤:

  • 该类型必须实现 unittest.prop_test 包中的 Arbitrary 接口
  • 如果您还想支持缩减值,则还必须实现 unittest.prop_test 包中的 Shrink 接口

示例如下:

from std import unittest.prop_test.*
from std import random.Random

class SimpleClass <: ToString {
    SimpleClass(let name: String, let number: Int64) {}

    public func toString(): String {
        "SimpleClass(${name}, ${number})"
    }
}

extend SimpleClass <: Arbitrary<SimpleClass> {
    public static func arbitrary(random: Random): Generator<SimpleClass> {
        let stringGenerator = String.arbitrary(random)
        let numberGenerator = Int64.arbitrary(random)
        return Generators.generate {
            SimpleClass(stringGenerator.next(), numberGenerator.next())
        }
    }
}

@Test[x in random()]
@Configure[randomSeed: 0]
func foo(x: SimpleClass): Unit {
    // we can now write tests for SimpleClass
    @Expect(x.name.size != x.number)
}

输出如下

--------------------------------------------------------------------------------------------------
TP: default, time elapsed: 2600034 ns, Result:
    TCS: TestCase_foo, time elapsed: 2596678 ns, RESULT:
    [ FAILED ] CASE: foo (2587608 ns)
    REASON: After 53 generation steps
    x = SimpleClass(_, 1)
    with randomSeed = 0

    Expect Failed: `(x . name . size != x . number == true)`
       left: false
      right: true

Summary: TOTAL: 1
    PASSED: 0, SKIPPED: 0, ERROR: 0
    FAILED: 1, listed below:
        TCS: TestCase_foo, CASE: foo
--------------------------------------------------------------------------------------------------

请注意,随机生成不一定实现 ToString,但如果没有它,它将无法在测试输出中打印生成的值。

增加新的数据生成策略

为了为参数化测试创建新的数据值源,需要执行以下步骤:

  • 相应的类型必须实现 unittest.common 包中的 DataStrategy<T> 接口,其中 T 是您要支持的参数类型。
  • 涉及此类类型的任何表达式都可以在数据 DSL 中用作值源。

示例如下:

class SimpleStorage {
    SimpleStorage() {}
    let v1: String = "1"
    let v2: String = "2"
    let v3: String = "3"
    let v4: String = "4"
}

extend SimpleStorage <: DataProvider<String> {
    public func provide(): Iterable<String> {
        return [v1, v2, v3, v4]
    }
    public prop isInfinite: Bool {
        get() { false }
    }
}

extend SimpleStorage <: DataStrategy<String> {
    public func provider(_: Configuration): DataProvider<String> {
        return this
    }
}

// SimpleStorage can now be used in data DSL as a source of String
@Test[x in SimpleStorage()]
func foo(x: String): Unit {

    @Expect(x.size > 0)
}

附录

API list

在框架中,部分 API 由于整体实现结构要求,属性为对外可见,但用户不应直接使用如下 API 。因此此处仅列举 API 列表,而不详细说明对应 API 使用方式。

package unittest

public class AssertException <: Exception {
    public init()
    public init(message: String)
}
public class AssertIntermediateException <: Exception {
    public let expression: String
    public let position: Int64
    public let originalException: Exception
    public func getOriginalStackTrace(): String
}
public open class UnittestException <: Exception
public class UnittestCliOptionsFormatException <: UnittestException

public func assertEqual<T>(leftStr:String, rightStr:String, expected: T, actual: T): Unit where T <: Equatable<T>
public func assertEqual<T, C>(leftStr:String, rightStr:String, expected: C, actual: C): Unit where T <: Equatable<T>, C <: Array<T>
public func expectEqual<T>(leftStr:String, rightStr:String, expected: T, actual: T): Unit where T <: Equatable<T>
public func expectEqual<T, C>(leftStr:String, rightStr:String, expected: C, actual: C): Unit where T <: Equatable<T>, C <: Array<T>

public class BenchParameterTestCase<T> <: ParameterTestCase<T>{
    public init(
        isSkip!: Bool,
        caseName!: String,
        argumentNames!: Array<String>,
        doRunBatch!: (T, Int64, Int64) -> Unit,
        strategy!: DataStrategy<T>,
        configuration!: Configuration,
        timeout!: TimeoutInfo
    ) 
}

public class CombinedDataFinisher2<T0, T1> <: DataFinisher<(T0, T1)> {
    public override func finish(value: (T0, T1))
}
public class CombinedDataFinisher3<T0, T1, T2> <: DataFinisher<(T0, T1, T2)> {
    public override func finish(value: (T0, T1, T2))
}
public class CombinedDataFinisher4<T0, T1, T2, T3> <: DataFinisher<(T0, T1, T2, T3)> {
    public override func finish(value: (T0, T1, T2, T3))
}
public class CombinedDataFinisher5<T0, T1, T2, T3, T4> <: DataFinisher<(T0, T1, T2, T3, T4)> {
    public override func finish(value: (T0, T1, T2, T3, T4))
}

public class CombinedDataProvider2<T0, T1> <: DataProvider<(T0, T1)> {
    public func positions(): Array<Int64>
    public prop isInfinite: Bool
    public func provide(): Iterator<(T0, T1)>
}
public class CombinedDataProvider3<T0, T1, T2> <: DataProvider<(T0, T1, T2)> {
    public func positions(): Array<Int64>
    public prop isInfinite: Bool
    public func provide(): Iterator<(T0, T1, T2)>
}
public class CombinedDataProvider4<T0, T1, T2, T3> <: DataProvider<(T0, T1, T2, T3)> {
    public func positions(): Array<Int64> { pos }
    public prop isInfinite: Bool
    public func provide(): Iterator<(T0, T1, T2, T3)>
}
public class CombinedDataProvider5<T0, T1, T2, T3, T4> <: DataProvider<(T0, T1, T2, T3, T4)> {
    public func positions(): Array<Int64> { pos }
    public prop isInfinite: Bool
    public func provide(): Iterator<(T0, T1, T2, T3, T4)>
}

public class CombinedDataShrinker2<T0, T1> <: DataShrinker<(T0, T1)> {
    public override func shrink(value: (T0, T1)): Iterable<(T0, T1)>
}
public class CombinedDataShrinker3<T0, T1, T2> <: DataShrinker<(T0, T1, T2)> {
    public override func shrink(value: (T0, T1, T2)): Iterable<(T0, T1, T2)>
}
public class CombinedDataShrinker4<T0, T1, T2, T3> <: DataShrinker<(T0, T1, T2, T3)> {
     public override func shrink(value: (T0, T1, T2, T3)): Iterable<(T0, T1, T2, T3)> 
}
public class CombinedDataShrinker5<T0, T1, T2, T3, T4> <: DataShrinker<(T0, T1, T2, T3, T4)> {
    public override func shrink(value: (T0, T1, T2, T3, T4)): Iterable<(T0, T1, T2, T3, T4)> 
}

public class CombinedDataStrategy2<T0, T1> <: FormattedDataStrategy<(T0, T1)> {

    public init(t0Strategy: DataStrategy<T0>, t1Strategy: DataStrategy<T1>)
    public override func provider(configuration: Configuration) 
    public override func shrinker(configuration: Configuration)
    public override func finisher(configuration: Configuration) 
}
public class CombinedDataStrategy3<T0, T1, T2> <: FormattedDataStrategy<(T0, T1, T2)> {
    public init(
        t0Strategy: DataStrategy<T0>,
        t1Strategy: DataStrategy<T1>,
        t2Strategy: DataStrategy<T2>)
    public override func provider(configuration: Configuration) 
    public override func shrinker(configuration: Configuration) 
    public override func finisher(configuration: Configuration)
}
public class CombinedDataStrategy4<T0, T1, T2, T3> <: FormattedDataStrategy<(T0, T1, T2, T3)> {
    public init(
        t0Strategy: DataStrategy<T0>,
        t1Strategy: DataStrategy<T1>,
        t2Strategy: DataStrategy<T2>,
        t3Strategy: DataStrategy<T3>)
    public override func provider(configuration: Configuration)
    public override func shrinker(configuration: Configuration)
    public override func finisher(configuration: Configuration)
}
public class CombinedDataStrategy5<T0, T1, T2, T3, T4> <: FormattedDataStrategy<(T0, T1, T2, T3, T4)> {
    public init(
        t0Strategy: DataStrategy<T0>,
        t1Strategy: DataStrategy<T1>,
        t2Strategy: DataStrategy<T2>,
        t3Strategy: DataStrategy<T3>,
        t4Strategy: DataStrategy<T4>)
    public override func provider(configuration: Configuration)
    public override func shrinker(configuration: Configuration)
    public override func finisher(configuration: Configuration)
}

public func checkDataStrategy<T>(strategy: DataStrategy<T>): DataStrategy<T>{
    public func provider(configuration: Configuration): DataProvider<T>{ val.provider(configuration) }
    public func shrinker(configuration: Configuration): DataShrinker<T> { val.shrinker(configuration) }
    public func finisher(configuration: Configuration): DataFinisher<T> { val.finisher(configuration) }
}
 
public sealed abstract class FormattedDataStrategy<T> <: DataStrategy<T>

public func defaultConfiguration(): Configuration

public func entryMain(cases: TestPackage): Int64

public class SerializableProvider<T> <: DataProvider<T> where T <: Serializable<T> {
    public override func provide(): Iterable<T>
    public prop isInfinite: Bool
}

/**********************************/

public open class ParameterTestCase<T> <: UTestCase {
    public override func run(ctx: TestCaseContext): Unit
}
public func makeParameterTestCase<T>(
    caseName!: String,
    isSkip!: Bool,
    argumentNames!: Array<String>,
    doRun!: (T) -> Unit,
    strategy!: DataStrategy<T>,
    configuration!: Configuration,
    timeout!: TimeoutInfo
): ParameterTestCase<T> 
 
public class PowerAssertDiagramBuilder {
    public init(expression: String, initialPosition: Int64)
    public func r<T>(value: T, exprAsText: String, position: Int64): T
    public func r(value: String, exprAsText: String, position: Int64): String
    public func h(exception: Exception, exprAsText: String, position: Int64): Nothing
    public func w(result: Bool)
}

public func pprint(pp: PrettyPrinter): PrettyPrinter

public abstract class UTestCase <: Hashable & Equatable<UTestCase>{
    public init(caseName: String, isSkip!: Bool, isBench!: Bool, timeout!: TimeoutInfo)
    public func run(ctx: TestCaseContext): Unit
    public func hashCode() : Int64
    public operator func ==(rhs: UTestCase): Bool
    public operator func !=(rhs: UTestCase): Bool
}

public class SimpleTestCase <: UTestCase {
    public init(
        isSkip: Bool, caseName: String, doRun: () -> Unit,
        times!: Int64 = 1, isBench!: Bool = false, timeout!: TimeoutInfo = NoTimeout)
    public init(
        caseName: String, doRun: () -> Unit, times!: Int64 = 1,
        isBench!: Bool = false, timeout!: TimeoutInfo = NoTimeout)

    public override func run(ctx: TestCaseContext): Unit
}
public open class TestCases <: UTest {
    public func addCase(t: UTestCase): Unit
    public func withName(name: String): This
    public init()
    public init(name: String)
    public init(name: String, isParallel!: Bool)
    public func loadCases(): Unit
}

public open class TestInfo <: Serializable<TestInfo> & PrettyPrintable {
    public var config: Configuration = Configuration()
    public var name: String
    public prop errorCount: Int64
    public prop caseCount: Int64 
    public prop passedCount: Int64
    public prop failedCount: Int64 
    public prop timeoutCount: Int64
    public prop skippedCount: Int64 
    public func recordStartTime(): Unit 
    public func recordTimeDuration(): Unit
    public func printResult(): Unit 
    public open override func pprint(pp: PrettyPrinter): PrettyPrinter 
    public func getBenchResults(): HashMap<String, (Float64,Float64)> 
    public open func serialize(): DataModel 
    public static func deserialize(dm: DataModel): TestInfo
    public func addCheckResult(check: CheckResult)
    public func serialize(): DataModel
    public static func deserialize(dm: DataModel): TestResult 
    public override func pprint(pp: PrettyPrinter): PrettyPrinter
}
 
public open class CheckResult <: Serializable<CheckResult> {
    public func serialize(): DataModel
    public static func deserialize(dm: DataModel): addCheckResult
}

public class TestCaseContext {}

public class TestPackage <: UTest {
    public init(packageName: String)
    public func add(t: TestCases): TestPackage
    public func add(elements: Collection<TestCases>): TestPackage 
    public func runCases(): UnittestCliOptionsFormatException
}

public class TestModule <: UTest {
    public init(moduleName: String)
    public func add(t: TestPackage): TestModule 
    public func add(elements: Collection<TestPackage>): TestModule
    public func runCases(): Unit
}

public class TestPackageInfo <: TestInfo & Serializable<TestPackageInfo> {
    public override func serialize(): DataModel
    static public redef func deserialize(dm: DataModel): TestPackageInfo
    public override func pprint(pp: PrettyPrinter): PrettyPrinter
}
public class TestModuleInfo <: TestInfo & Serializable<TestModuleInfo> {
    public override func serialize(): DataMode
    static public redef func deserialize(dm: DataModel): TestModuleInfo
    public override func pprint(pp: PrettyPrinter): PrettyPrinter 
}

public abstract class UTest {
    public func execute(): Unit 
    public func printResult(): Unit
    public open func jsonReport(): JsonValue
    public open func getTestInfo(): TestInfo 
    public open prop ctx: TestCaseContext 
}

public class Configuration <: ToString {
    public init()
    public func get<T>(key: String): ?T 
    public func set<T>(key: String, value: T) 
    public func remove<T>(key: String): ?T
    public func clone(): Configuration 
    public func toString(): string
}

package unittest.common

public interface DataProvider<T> {
    func provide(): Iterable<T>
    func positions(): Array<Int64> /* users should not implement this API */
    prop isInfinite: Bool
}

public enum Color {
    | RED
    | GREEN
    | YELLOW
    | BLUE
    | CYAN
    | MAGENTA
    | GRAY
    | DEFAULT_COLOR
}

public abstract class PrettyPrinter {
    public init(indentationSize!: UInt64 = 4, startingIndent!: UInt64 = 0)
    public prop isTopLevel: Bool 
    public func indent(body: () -> Unit): PrettyPrinter 
    public func indent(indents: UInt64, body: () -> Unit): PrettyPrinter 
    public func customOffset(symbols: UInt64, body: () -> Unit): PrettyPrinter 
    public func colored(color: Color, body: () -> Unit): PrettyPrinter
    public func colored(color: Color, text: String): PrettyPrinter
    public func append(text: String): PrettyPrinter
    public func appendCentered(text: String, space: UInt64): PrettyPrinter
    public func append<PP>(value: PP): PrettyPrinter where PP <: PrettyPrintable 
    public func newLine(): PrettyPrinter
    public func appendLine(text: String): PrettyPrinter
    public func appendLine<PP>(value: PP): PrettyPrinter where PP <: PrettyPrintable
}
public class PrettyText <: PrettyPrinter & PrettyPrintable & ToString {
     public init()
     public init(string: String)
     public static func of<PP>(pp: PP) where PP <: PrettyPrintable 
     public func isEmpty(): Bool 
     public func pprint(to: PrettyPrinter): PrettyPrinter
     public func toString(): String
}

public interface PrettyPrintable {
    func pprint(to: PrettyPrinter): PrettyPrinter
}

extend Array<T> <: PrettyPrintable where T <: PrettyPrintable
extend ArrayList<T> <: PrettyPrintable where T <: PrettyPrintable

package unittest.prop_test

public func emptyIterable<T>(): Iterable<T>

public interface Generator<T> {
    func next(): T
}

public class Generators {
    public static func single<T>(value: T): Generator<T>
    public static func generate<T>(body: () -> T): Generator<T>
    public static func iterable<T>(random: Random, collection: Array<T>): Generator<T> 
    public static func weighted<T>(random: Random, variants: Array<(UInt64, Generator<T>)>): Generator<T>
    public static func pick<T>(random: Random, variants: Array<Generator<T>>): Generator<T>
    public static func lookup<T>(random: Random): Generator<T> where T <: Arbitrary<T> 
    public static func mapped<T, R>(random: Random, body: (T) -> R): Generator<R> where T <: Arbitrary<T> 
    public static func mapped<T1, T2, R>(random: Random, body: (T1, T2) -> R): Generator<R> where T1 <: Arbitrary<T1>, T2 <: Arbitrary<T2>
    public static func mapped<T1, T2, T3, R>(random: Random, body: (T1, T2, T3) -> R): Generator<R>
            where T1 <: Arbitrary<T1>, T2 <: Arbitrary<T2>, T3 <: Arbitrary<T3>
    public static func mapped<T1, T2, T3, T4, R>(random: Random, body: (T1, T2, T3, T4) -> R): Generator<R>
            where T1 <: Arbitrary<T1>, T2 <: Arbitrary<T2>, T3 <: Arbitrary<T3>, T4 <: Arbitrary<T4>
}

public class LazySeq<T> <: Iterable<T> {
    public static func of(iterable: Iterable<T>)
    public static func of(array: Array<T>)
    public init(element: T)
    public init()
    public func iterator(): Iterator<T>
    public func concat(other: LazySeq<T>): LazySeq<T>
    public func prepend(element: T): LazySeq<T>
    public func append(element: T): LazySeq<T>
    public func mixWith(other: LazySeq<T>): LazySeq<T> 
    public static func mix(l1: LazySeq<T>, l2: LazySeq<T>) 
    public static func mix(l1: LazySeq<T>, l2: LazySeq<T>, l3: LazySeq<T>)
    public static func mix(l1: LazySeq<T>, l2: LazySeq<T>, l3: LazySeq<T>, l4: LazySeq<T>)
    public static func mix(l1: LazySeq<T>, l2: LazySeq<T>, l3: LazySeq<T>, l4: LazySeq<T>, l5: LazySeq<T>) 
    public func map<U>(body: (T) -> U): LazySeq<U>
}

public class ShrinkHelpers {
    public static func shrinkTuple<T0, T1>
    public static func shrinkTuple<T0, T1, T2>
    public static func shrinkTuple<T0, T1, T2, T3>
    public static func shrinkTuple<T0, T1, T2, T3, T4>
}

public interface IndexAccess {
    func getElementAsAny(index: Int64): ?Any
}
public struct Function0Wrapper<R> {
    public Function0Wrapper(public let function: () -> R) {}
    public operator func () (): R { function() }
}
public struct TupleWrapper2<T0, T1> {
    public TupleWrapper2(public let tuple: (T0, T1)) 
    public func apply<R>(f: (T0, T1) -> R): R
}

public struct TupleWrapper3<T0, T1, T2> {
    public TupleWrapper3(public let tuple: (T0, T1, T2)) 
    public func apply<R>(f: (T0, T1, T2) -> R): R 
}
public struct TupleWrapper4<T0, T1, T2, T3> {
    public TupleWrapper4(public let tuple: (T0, T1, T2, T3)) 
    public func apply<R>(f: (T0, T1, T2, T3) -> R): R
}
public struct TupleWrapper5<T0, T1, T2, T3, T4> {
    public TupleWrapper5(public let tuple: (T0, T1, T2, T3, T4))
    public func apply<R>(f: (T0, T1, T2, T3, T4) -> R): R 
}

/* how to describe the specification for function and tuples?*/
extend Function0Wrapper<R> <: Arbitrary<Function0Wrapper<R>> where R <: Arbitrary<R>
extend TupleWrapper2<T0, T1> <: Arbitrary<TupleWrapper2<T0, T1>>
extend TupleWrapper3<T0, T1, T3> <: Arbitrary<TupleWrapper3<T0, T1, T3>>
extend TupleWrapper4<T0, T1, T3, T4> <: Arbitrary<TupleWrapper4<T0, T1, T3, T4>>
extend TupleWrapper5<T0, T1, T3, T4, T5> <: Arbitrary<TupleWrapper5<T0, T1, T3, T4, T5>>
extend Function0Wrapper<R> <: Shrink<Function0Wrapper<R>> {}
extend TupleWrapper2<T0, T1> <: Shrink<TupleWrapper2<T0, T1>>
extend TupleWrapper3<T0, T1, T3> <: Shrink<TupleWrapper3<T0, T1, T3>>
extend TupleWrapper4<T0, T1, T3, T4> <: Shrink<TupleWrapper4<T0, T1, T3, T4>>
extend TupleWrapper5<T0, T1, T3, T4, T5> <: Shrink<TupleWrapper5<T0, T1, T3, T4, T5>>

extend TupleWrapper2<T0, T1> <: IndexAccess
extend TupleWrapper3<T0, T1, T3> <: IndexAccess
extend TupleWrapper4<T0, T1, T3, T4> <: IndexAccess
extend TupleWrapper5<T0, T1, T3, T4, T5> <: IndexAccess

package unittest.testmacro

class MacroException <: Exception {
    public init()
    public init(message: String)
}