Unittest相关概念

测试及测试用例

测试是用 @Test 宏标记的实体,会在测试过程中执行。 仓颉 unittest 框架中有两种测试:测试类和测试函数。 测试函数相对简单,每个函数包含全部测试运行的代码。 测试类适用于为测试引入更深层次的结构,或者覆盖测试生命周期行为的场景。

每个测试类由若干个测试用例组成,每个测试用例用 @TestCase 宏标记。 每个测试用例都是测试类内部的一个函数。 上一节中的示例,相同的测试可以改写为如下所示的测试类:

@Test
class AddTests {
    @TestCase
    func addTest() {
        @Expect(add(2, 3), 5)
    }

    @TestCase
    func addZero() {
        @Expect(add(2, 0), 2)
    }
}

测试函数即函数中包含单个测试用例的测试。这种情况下不需要使用 @TestCase 宏。

cjpm test 中运行这个新的测试类会生成如下类似输出:

--------------------------------------------------------------------------------------------------
TP: example/example, time elapsed: 67369 ns, Result:
    TCS: AddTests, time elapsed: 31828 ns, RESULT:
    [ PASSED ] CASE: addTest (25650 ns)
    [ PASSED ] CASE: addZero (4312 ns)
    Summary: TOTAL: 2
    PASSED: 2, SKIPPED: 0, ERROR: 0
    FAILED: 0
--------------------------------------------------------------------------------------------------
cjpm test success

断言

断言是在测试用例函数体内执行的单个条件检查,用以判断代码是否正常运行。 断言有两种:@Expect@Assert 。 创建一个错误的测试来说明两者的区别:

@Test
func testAddIncorrect() {
    @Expect(add(3, 3), 5)
}

运行测试失败,并生成以下结果(仅展示与此测试相关的部分):

    TCS: TestCase_testAddIncorrect, time elapsed: 4236 ns, RESULT:
    [ FAILED ] CASE: testAddIncorrect (3491 ns)
    Expect Failed: `(add ( 3 , 3 ) == 5)`
       left: 6
      right: 5

在这个例子中,用 @Assert 替换 @Expect 不会有什么变化。添加一个检查项后,再次运行:

@Test
func testAddIncorrect() {
    @Expect(add(3, 3), 5)
    @Expect(add(5, 3), 9)
}

运行测试失败,并生成以下结果(仅展示与此测试相关的部分):

    TCS: TestCase_testAddIncorrect, time elapsed: 5058 ns, RESULT:
    [ FAILED ] CASE: testAddIncorrect (4212 ns)
    Expect Failed: `(add ( 3 , 3 ) == 5)`
       left: 6
      right: 5

    Expect Failed: `(add ( 5 , 3 ) == 9)`
       left: 8
      right: 9

可以在输出中看到这两个检查的结果。 但是,如果用 @Assert 替换 @Expect

@Test
func testAddIncorrectAssert() {
    @Assert(add(3, 3), 5)
    @Assert(add(5, 3), 9)
}

可以得到以下输出:

    TCS: TestCase_testAddIncorrectAssert, time elapsed: 31653 ns, RESULT:
    [ FAILED ] CASE: testAddIncorrectAssert (30893 ns)
    Assert Failed: `(add ( 3 , 3 ) == 5)`
       left: 6
      right: 5

可以看到,只有第一个 @Assert 检查失败了,其余的测试甚至都没有运行。 这是因为 @Assert 宏采用的是快速失败(fail-fast)机制:一旦首次断言失败,整个测试用例都失败,后续的断言不再检查。

在涉及大量断言的大型测试中,这一点非常重要,尤其是在循环中使用断言时。 并不需要等到全部失败,首次失败后用户即可感知。

在测试中选择 @Assert 还是 @Expect 取决于测试场景的复杂程度,以及是否需要采用快速失败机制。

使用 unittest 提供的两种断言宏时,可采用如下方式:

  • 相等断言,其中 @Assert(a, b)@Expect(a, b) 的两个参数 ab ,检查他们的参数值是否相等;假设 a 的类型为 Ab 的类型为 BA 必须实现了 Equatable<B>
  • 布尔断言 @Assert(c)@Expect(c) ,其参数 cBool 类型,参数值 truefalse

断言的第二种形式 @Assert(c) 可以视为 @Assert(c, true) 的简写形式。

测试生命周期

测试用例之间有时可以共享创建或清理代码。测试框架支持4个生命周期步骤,分别通过相应的宏来设置。只能为 @Test 测试类指定生命周期步骤,不能为 @Test 顶层函数指定生命周期步骤。

@BeforeAll在任何测试用例之前运行
@BeforeEach在每个测试用例之前运行一次
@AfterEach在每个测试用例之后运行一次
@AfterAll在所有测试用例完成后运行

这些宏必须配置在 @Test 测试类的成员或静态函数上。@BeforeAll@AfterAll 函数不能声明任何参数。 @BeforeEach@AfterEach 函数可以声明一个 String 类型的参数(或者不声明参数)。

@Test
class FooTest {
    @BeforeAll
    func setup() {
        //在测试执行前运行这段代码。
    }
}

每个宏可以应用于单个测试类内的多个函数。可以在单个函数上配置多个生命周期宏。生命周期宏不能配置在标有 @TestCase 或类似宏的函数上。

如果多个函数被标记为相同的生命周期步骤,则可以按照它们在代码中声明的顺序(从上到下)执行。

测试框架确保:

  1. 标记为 Before all 的步骤在任何测试用例运行之前,至少运行一次。
  2. 对于测试类中的每个测试用例 TC : 1) 标记为 Before each 的步骤在 TC 之前运行一次。 2) 运行 TC 。 3) 标记为 After each 的步骤在 TC 之后运行一次。
  3. 在测试类中的所有测试用例之后运行标记为 After all 的步骤。

注意,如果没有运行测试用例,上述并不适用。

简单场景中,标识为 before allafter all 的步骤只运行一次。但也有例外:

  • 对于类型参数化测试,标识为 before/after all 的步骤运行的数量为每个类型参数的组合数。
  • 如果多个测试用例在不同的进程中并行执行,则每个进程中标识为 before/after all 的步骤都将运行一次。

@BeforeEach@AfterEach 可以访问正在创建或删除的测试用例,只需要在相应的函数中指定一个 String 类型的参数即可。

@Test
class Foo {
    @BeforeEach
    func prepareData(testCaseName: String) {
        //测试用例函数的名称作为参数
        //本例中的"bar"
    }

    @AfterEach
    func cleanup() {
        //不指定参数也可以
    }

    @TestCase
    func bar() {}
}

参数化测试或参数化性能测试配置生命周期时,注意标识为 before eachafter each 的步骤仅在执行测试用例或基准之前或之后为其所有参数执行一次。也就是说,从生命周期的角度看,使用不同参数执行多次的测试主体会被视为单个测试用例。

如果参数化测试的每个参数都需要单独创建清理,需要将相应的代码配置在测试用例主体本身中。此外,还可以访问参数本身。

测试配置

单元测试框架中其他更高级的功能可能需要额外配置。 参考如下三种方式配置测试:

  • 使用 @Configure
  • 直接在运行测试时或在 cjpm test 测试中使用命令行参数
  • 使用 cjpm 配置文件