【问题标题】:Parametrized unit tests in SwiftSwift 中的参数化单元测试
【发布时间】:2016-05-28 05:42:20
【问题描述】:

有什么方法可以使用参数化单元测试,类似于在.Net 中使用NUnit 框架可以实现的。

[TestCase(12, 3, 4)]
[TestCase(12, 2, 6)]
[TestCase(12, 4, 3)]
public void DivideTest(int expectedResult, int a, int b)
{
  Assert.AreEqual(expectedResult, a / b);
}

使用这种测试(与非参数化测试相比)可以让您避免编写一系列几乎相同的单元测试,仅在参数值上有所不同,从而为您带来更大的回报。

我正在寻找基于 XCTest 的解决方案或其他一些方法来实现它。最佳解决方案应该在 Xcode 中将每个测试用例(参数集)报告为单独的单元测试,这样是否所有或仅部分测试用例都失败了。

【问题讨论】:

  • 您可以将部分测试方法提取到辅助方法中。如果您可以阅读 ObjC,这可能会有所帮助:qualitycoding.org/refactoring-tests
  • 遗憾的是,我们只能破解变通办法,因为 XCTest 仍然 没有参数化测试。

标签: swift unit-testing parameterized-unit-test


【解决方案1】:

我们发现此解决方案How to Dynamically add XCTestCase 为我们提供了所需的灵活性。能够动态添加测试,将参数传递给测试,并在测试报告中显示动态测试名称。

另一个选择是在 XCode 中检查 XCTestPlan。 There's an informative video from WWDC about it.

【讨论】:

    【解决方案2】:

    我更喜欢@DariusVsolution。但是,当我开发人员直接从 Xcode 的侧边栏执行测试方法时,它并不能很好地处理。这对我来说是个大问题。

    我认为我最终做的事情相当巧妙。

    我声明testValuesDictionary(可能需要一个更好的名称)作为我的XCTestCase 子类的实例计算属性。然后,我定义了一个 Dictionary 输入文字,用于键入预期的输出。我的示例测试了一个作用于Int 的函数,所以我这样定义testValues

    static var testRange: ClosedRange<Int> { 0...100 }
    
    var testValues: [Int: Int] {
        let range = Self.testRange
        return [
            // Lower extreme
            Int.min: range.lowerBound,
    
            // Lower bound
            range.lowerBound - 1: range.lowerBound,
            range.lowerBound    : range.lowerBound,
            range.lowerBound + 1: range.lowerBound + 1,
    
            // Middle
            25: 25,
            50: 50,
            75: 75,
    
            // Upper bound
            range.upperBound - 1: range.upperBound - 1,
            range.upperBound    : range.upperBound,
            range.upperBound + 1: range.upperBound,
    
            // Upper extreme
            Int.max: range.upperBound
        ]
    }
    

    在这里,我很容易声明我的边缘和边界情况。一种更语义化的方法可能是使用元组数组,但 Swift 的字典文字语法足够薄,我知道这是做什么的。 ?

    现在,在我的测试方法中,我有一个简单的for 循环。

    /// The property wrapper I'm testing. This could be anything, but this works for example.
    @Clamped(testRange) var number = 50
    
    func testClamping() {
        let initial = number
    
        for (input, expected) in testValues {
            // Call the code I'm testing. (Computed properties act on assignment)
            number = input
            XCTAssertEqual(number, expected, "{number = \(input)}: `number` should be \(expected)")
    
            // Reset after each iteration.
            number = initial
        }
    }
    

    现在要为每个参数运行,我只需以 Xcode 的任何常规方式或任何其他适用于 Linux 的方式(我假设)调用 XCTests。无需运行每个测试类来获得这个参数化。

    这不是很漂亮吗?我只是在几行DRY 代码中涵盖了每个边界值和等价类!

    至于识别失败的情况,每次调用都通过一个XCTAssert 函数运行,根据 Xcode 的约定,只有在您需要考虑错误时才会向您发送消息。这些显示在侧边栏中,但类似的消息往往会融合在一起。我在这里的消息字符串标识了失败的输入及其结果输出,修复了混合在一起,并使我的测试流程成为一个理智的樱桃苹果派。 (乡巴佬,你可以用任何你喜欢的方式格式化你的格式!任何能保佑你理智的东西。)

    很好吃。

    TL;DR

    改编自@Code Differentanswer:使用输入和输出字典,并使用for 循环运行。 ?

    【讨论】:

    • 简单的on循环枚举获取索引,为什么还要多取一个obj来维护当前索引?
    • @nikhilnangia I... 不维护当前索引。我不确定你是什么意思;循环给了我字典中的当前键和值。我使用输入键调用我的代码,并将输出与预期输出值进行比较。 initial 值在循环之前设置,以便我可以重置被测单元的状态。我在这里看不到任何索引变量。
    • 为什么testRange 是计算属性?这是完全没有必要的。只需使用存储的属性。 testValues 也不需要是计算属性。两个属性总是返回相同的值。
    • @PeterSchorn 你是对的。 static let 应该可以正常工作。
    • @SeizeTheDay 很高兴我可以帮助改进您的代码。此外,您应该尽可能避免在测试类中使用实例属性。信不信由你,XCTest 实际上为每个测试方法创建了一个单独的测试类实例。见stackoverflow.com/a/48562936/12394554。因此,您应该改用静态/类属性。
    【解决方案3】:

    断言似乎都是throw,所以也许这样的东西对你有用:

    typealias Division = (dividend: Int, divisor: Int, quotient: Int)
    
    func testDivisions() {
        XCTAssertNoThrow(try divisionTest((12, 3, 4)))
        XCTAssertNoThrow(try divisionTest((12, 2, 6)))
        XCTAssertNoThrow(try divisionTest((12, 4, 3)))
    }
    
    private func divisionTest(_ division: Division) throws {
        XCTAssertEqual(division.dividend / division.divisor, division.quotient)
    }
    

    如果一个失败,整个函数都会失败。如果需要更多粒度,可以将每个案例拆分为一个单独的函数。

    【讨论】:

      【解决方案4】:

      你的函数参数到处都是。我不确定你的函数是在做乘法还是除法。但这是一种可以在一个测试方法中执行多个测试用例的方法。

      给定这个函数:

      func multiply(_ a: Int, _ b: Int) -> Int {
          return a * b
      }
      

      你可以有多个测试用例:

      class MyTest: XCTestCase {
          func testMultiply() {
              let cases = [(4,3,12), (2,4,8), (3,5,10), (4,6,20)]
              cases.forEach {
                  XCTAssertEqual(multiply($0, $1), $2)
              }
          }
      }
      

      最后两个会失败,Xcode 会告诉你它们。

      【讨论】:

      • 这样做的一个主要缺点是,当第一个案例失败时,执行将停止。参数化测试在哪里继续进行,每个案例在报告中都被视为单独的测试,这样可以更容易地确定哪个确切的组合失败了。
      • @KevinR 你用过 XCTest 框架吗?使用XCTAssertXCTAssertEqual 而不仅仅是assert 的全部意义在于,在发生故障时不会停止执行。
      • 请永远不要使用forEach。它非常丑陋;只需使用 for 循环。
      • @PeterSchorn 测试用例的执行:是的。不执行该特定测试。如果不再是这种情况,则行为可能会随着时间的推移而改变。还;不要将您对代码风格的个人意见陈述为法律;没有人喜欢刻薄的人。
      • 当你说“那个特定的测试”时,你指的是一种测试方法吗?目前,XC 断言失败时永远不会停止执行,除非您将continueAfterFailure 设置为 false,在这种情况下,当前测试方法的执行将在失败发生时停止。
      【解决方案5】:

      使用参数化的最佳方式是使用 XCTestCase 子类的属性defaultTestSuite。下一个明确的例子是:

      import XCTest
      
      class ParameterizedExampleTests: XCTestCase {
      
          //properties to save the test cases
          private var array: [Float]? = nil
          private var expectedResult: Float? = nil
      
          // This makes the magic: defaultTestSuite has the set of all the test methods in the current runtime
          // so here we will create objects of ParameterizedExampleTests to call all the class' tests methodos
          // with differents values to test
          override open class var defaultTestSuite: XCTestSuite {
              let testSuite = XCTestSuite(name: NSStringFromClass(self))
              addTestsWithArray([12, 3], expectedResult: 4, toTestSuite: testSuite)
              addTestsWithArray([12, 2], expectedResult: 6, toTestSuite: testSuite)
              addTestsWithArray([12, 4], expectedResult: 3, toTestSuite: testSuite)
              return testSuite
          }
      
          // This is just to create the new ParameterizedExampleTests instance to add it into testSuite
          private class func addTestsWithArray(_ array: [Float], expectedResult: Float, toTestSuite testSuite: XCTestSuite) {
              testInvocations.forEach { invocation in
                  let testCase = ParameterizedExampleTests(invocation: invocation)
                  testCase.array = array
                  testCase.expectedResult = expectedResult
                  testSuite.addTest(testCase)
              }
          }
      
          // Normally this function is into production code (e.g. class, struct, etc).
          func division(a: Float, b: Float) -> Float {
              return a/b
          }
      
          func testDivision() {
              XCTAssertEqual(self.expectedResult, division(a: array?[0] ?? 0, b: array?[1] ?? 0))
          }
      }
      

      【讨论】:

      • 不幸的是,该套件的测试仅显示为 Xcode 测试导航器的单个测试。有解决办法吗?
      • @tcurdt 这实际上是一个正在运行的测试,但经过了多次验证,所以这应该显示为一个测试,但有多次调用,这是为了避免产生不必要的额外代码覆盖率
      • 当然 - 但无法深入到调用结果大大降低了这种方法的实用性。
      【解决方案6】:

      @Code Different 的回答是合法的。以下是其他两个选项,或者更确切地说是解决方法

      基于属性的测试

      您可以使用Fox 之类的工具来执行生成测试,其中测试框架将为您想要测试的行为生成许多输入集并为您运行它们。

      有关此方法的更多信息:

      BDD 共享示例

      如果您喜欢BDD 样式并且正在使用支持它们的测试框架,您可以使用共享示例

      使用Quick 看起来像:

      class MultiplySharedExamplesConfiguration: QuickConfiguration {
        override class func configure(configuration: Configuration) {
          sharedExamples("something edible") { (sharedExampleContext: SharedExampleContext) in
            it("multiplies two numbers") {
              let a = sharedExampleContext()["a"]
              let b = sharedExampleContext()["b"]
              let expectedResult = sharedExampleContext()["b"]
      
              expect(multiply(a, b)) == expectedResult
            }
          }
        }
      }
      
      class MultiplicationSpec: QuickSpec {
        override func spec() {
          itBehavesLike("it multiplies two numbers") { ["a": 2, "b": 3, "result": 6] }
          itBehavesLike("it multiplies two numbers") { ["a": 2, "b": 4, "result": 8] }
          itBehavesLike("it multiplies two numbers") { ["a": 3, "b": 3, "result": 9] }
        }
      }
      

      说实话,这个选项是:1) 大量工作,2) 共享示例技术的滥用,因为您不是使用它们来测试多个类共享的行为,而是对测试进行参数化。但正如我一开始所说,这更像是一种解决方法。

      【讨论】:

      • 我喜欢生成测试的一般概念。也许它可以与 ObjC 运行时结合使用来实现测试参数化。绝对是一个很好的灵感来源。
      猜你喜欢
      • 1970-01-01
      • 2016-12-04
      • 2014-01-20
      • 2011-05-22
      • 2012-07-24
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多