Swift Testing #5: 使用 arguments 参数化测试

发布: (2025年12月5日 GMT+8 06:27)
6 min read
原文: Dev.to

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 可以使用集合进行参数化。这样,测试会针对集合中的每个元素单独执行一次。采用这种方式,测试报告会把所有场景分别列出,并且可以只运行特定条件的测试,而无需执行全部。

要参数化测试,只需在测试方法中添加一个参数,并在 @Testarguments 参数中提供值集合(参见文档)。有了这些改动,报告中会把每个条件单独显示,就像独立的测试一样。

@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

参考文献

  • 视频 “Mastering Swift Testing: Eliminate Duplicate Tests with Parameterized Testing” (Swift and Tips),这里
  • Swift Testing 文档,这里
  • “Implementing parameterized tests” 文档,这里
  • 文章 “Swift Parameterized Testing”,这里
  • 文章 “Introducing Swift Testing. Parameterized Tests.”,这里
Back to Blog

相关文章

阅读更多 »