Swift Testing #5: 使用 arguments 参数化测试
Source: Dev.to
参数化单一条件
有时需要验证接受参数的代码在使用多个值时的行为。传统的做法是构建一个集合,然后遍历它。
@Test("Even Value")
func even() {
let values = [2, 8, 50]
values.forEach { value in
#expect(value.isMultiple(of: 2))
}
}
虽然上述实现可以工作,但它有一些缺点:
- 在报告中只显示测试执行的整体结果。也就是说:如果执行了 1000 种场景,只有一种失败,报告只会显示测试失败。
- 不能只针对特定条件重复测试,必须执行所有迭代。
针对这些问题,Swift Testing 的 @Test 可以使用集合进行参数化。这样,测试会针对集合中的每个元素单独执行一次。采用这种方式,测试报告会把所有场景分别列出,并且可以只运行特定条件的测试,而无需执行全部。
要参数化测试,只需在测试方法中添加一个参数,并在 @Test 的 arguments 参数中提供值集合(参见文档)。有了这些改动,报告中会把每个条件单独显示,就像独立的测试一样。
@Test("Even Value", arguments: [2, 8, 50])
func even(value: Int) {
#expect(value.isMultiple(of: 2))
}
测试参数组合
在上一个场景的基础上,我们想要通过组合两个集合的条件来测试系统。手动实现时会嵌套两个循环:
@Test("Even Value")
func even() {
let values1 = [2, 8, 50]
let values2 = [3, 6, 9]
values1.forEach { value1 in
values2.forEach { value2 in
let multiplication = value1 * value2
#expect(multiplication.isMultiple(of: 2))
}
}
}
然而,我们可以利用 arguments 的重载来直接传入两个数据集合(参见文档)。
@Test("Even Value", arguments: [2, 8, 50], [3, 6, 9])
func even(first: Int, second: Int) {
let multiplication = first * second
#expect(multiplication.isMultiple(of: 2))
}
在上述“测试 + 条件”系统中,总共有九种场景。但如果我们不想测试所有组合,只想逐一使用相同的索引该怎么办?
通过元组配对的条件
如果不进行组合,需要一个仅包含 N 条件的单一输入集合(N > 1)。每个条件由 M 个值的元组表示,这些值在测试方法中解构为 M 个参数。
@Test("Even Value", arguments: [(2, 3, true), (3, 5, false)])
func even(first: Int, second: Int, expectedResult: Bool) {
let multiplication = first * second
#expect(multiplication.isMultiple(of: 2) == expectedResult)
}
由于是一对一的组合方式,可以使用 zip 操作符将两个集合生成一个 Zip2Sequence 类型的集合。这使得可以分别标识两个集合,从而让测试更易读:
@Test("Even Value", arguments: [zip([2, 3], [3, 4])])
func even(first: Int, second: Int, expectedResult: Bool) {
let multiplication = first * second
#expect(multiplication.isMultiple(of: 2) == expectedResult)
}
上述实现的问题在于 @Test 只能组合两个集合(参见文档)。因此,如果需要组合超过两个集合,最好创建一个由 M 元素元组组成的单一集合(M > 2)。
通过结构体配对的条件
使用元组配对同一条件的值是解决多集合组合的简易办法,但也可以使用结构体来实现相同效果。结构体的属性对应构成测试条件的各个值。
struct TestModel {
let first: Int
let second: Int
let result: Int
}
@Test(arguments: [
TestModel(first: 1, second: 2, result: 3),
TestModel(first: 2, second: 3, result: 5),
TestModel(first: 3, second: 4, result: 7),
])
func basic(testModel: TestModel) {
let result = testModel.first + testModel.second
#expect(result == testModel.result)
}
这种写法不仅在构建测试时更易读,还可以让结构体遵循 CustomTestStringConvertible 协议,以提供 testDescription 描述,从而增加可读性。
struct TestModel: CustomTestStringConvertible {
let first: Int
let second: Int
let result: Int
var testDescription: String {
"\(first) + \(second) 应该等于 \(result)"
}
}
@Test(arguments: [
TestModel(first: 1, second: 2, result: 3),
TestModel(first: 2, second: 3, result: 5),
TestModel(first: 3, second: 4, result: 7),
])
func basic(testModel: TestModel) {
let result = testModel.first + testModel.second
#expect(result == testModel.result)
}
在这种情况下,报告会显示每个案例的可读描述,例如:
1 + 2 应该等于 3
2 + 3 应该等于 5
3 + 4 应该等于 7