Swift中的参数化单元测试

时间:2016-02-16 19:13:23

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

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

[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中的单独单元测试进行报告,因此是否所有测试用例或仅部分测试用例都失败了。

6 个答案:

答案 0 :(得分:7)

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

鉴于此功能:

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

答案 1 :(得分: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))
    }
}

答案 2 :(得分:3)

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

我认为我所做的事情很圆滑。

我声明了Dictionary中的testValues(问题需要一个更好的名称)作为我的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循环。 ?

答案 3 :(得分: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)滥用共享示例技术,因为你没有使用它们来测试多个类共享的行为,而是参数化测试。但正如我在开始时所说,这是一个更多的解决方法。

答案 4 :(得分: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)
}

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

答案 5 :(得分:0)

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