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)
的两个参数a
和b
,检查他们的参数值是否相等;假设a
的类型为A
,b
的类型为B
,A
必须实现了Equatable<B>
。 - 布尔断言
@Assert(c)
或@Expect(c)
,其参数c
为Bool
类型,参数值true
或false
。
断言的第二种形式 @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
或类似宏的函数上。
如果多个函数被标记为相同的生命周期步骤,则可以按照它们在代码中声明的顺序(从上到下)执行。
测试框架确保:
- 标记为 Before all 的步骤在任何测试用例运行之前,至少运行一次。
- 对于测试类中的每个测试用例
TC
: 1) 标记为 Before each 的步骤在TC
之前运行一次。 2) 运行TC
。 3) 标记为 After each 的步骤在TC
之后运行一次。 - 在测试类中的所有测试用例之后运行标记为 After all 的步骤。
注意,如果没有运行测试用例,上述并不适用。
简单场景中,标识为 before all 和 after 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 each 或 after each 的步骤仅在执行测试用例或基准之前或之后为其所有参数执行一次。也就是说,从生命周期的角度看,使用不同参数执行多次的测试主体会被视为单个测试用例。
如果参数化测试的每个参数都需要单独创建清理,需要将相应的代码配置在测试用例主体本身中。此外,还可以访问参数本身。
测试配置
单元测试框架中其他更高级的功能可能需要额外配置。 参考如下三种方式配置测试:
- 使用
@Configure
宏 - 直接在运行测试时或在
cjpm test
测试中使用命令行参数 - 使用
cjpm
配置文件