15

有什么方法可以使用参数化单元测试,类似于您可以在 .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 中的单独单元测试,因此是否所有测试用例或仅部分测试用例都失败了。

4

6 回答 6

14

使用参数化的最佳方法是使用 XCTestCase 子类的 property 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))
    }
}
于 2018-04-29T19:54:20.237 回答
11

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

鉴于此功能:

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 会告诉你它们。

于 2016-02-17T03:32:45.437 回答
3

我更喜欢@DariusV解决方案。但是,当我开发人员直接从 Xcode 的侧边栏执行测试方法时,它并不能很好地处理。这对我来说是一个交易破坏者。

我最终做的事情我认为是相当光滑的。

我将 a Dictionaryof testValues(probs 需要一个更好的名称) 声明为我的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;博士

@Code Different答案的改编:使用输入和输出字典,并for循环运行。

于 2020-02-10T18:40:52.113 回答
2

@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)对共享示例技术的滥用,因为您不是使用它们来测试多个类共享的行为,而是对测试进行参数化。但正如我一开始所说,这更像是一种解决方法。

于 2016-02-19T01:04:43.007 回答
0

我们发现这个解决方案如何动态添加 XCTestCase为我们提供了我们需要的灵活性。能够动态添加测试,将参数传递给测试,并在测试报告中显示动态测试名称。

另一种选择是在 XCode 中签出 XCTestPlan。 WWDC 有一个关于它的信息视频。

于 2019-10-16T17:34:25.960 回答
0

断言似乎都是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)
}

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

于 2019-08-27T13:16:27.833 回答