测试多个方法调用顺序

Testing multiple methods calling order

我有 3 个与特定 class 相关的方法,定义如下:

class MyClass: NSObject {
    func myMethod() {
        methodA()
        methodB()
        methodC()
    }
    func methodA() {}
    func methodB() {}
    func methodC() {}
}

我需要测试 myMethod 是否按实现顺序调用了所有 3 个方法:methodA 然后 methodB 然后 methodC 要使用 XCode 单元测试进行测试,无论这些方法的实现如何,我都在测试用例中创建了一个子 class,如下所示:

class ChildClass: MyClass {
    var method1CallingDate: Date?
    var method2CallingDate: Date?
    var method3CallingDate: Date?

    override func methodA() {
        super.methodA()
        method1CallingDate = Date()
    }
    override func methodB() {
        super.methodB()
        method2CallingDate = Date()
    }
    override func methodC() {
        super.methodC()
        method3CallingDate = Date()
    }
}

现在在测试方法中,我首先调用这 3 个方法,然后我首先断言所有三个日期都不为 nil,然后像这样比较它们:

XCTAssertLessThan(method1CallingDate, method2CallingDate)
XCTAssertLessThan(method2CallingDate, method3CallingDate)

我 运行 遇到的问题是测试有时成功有时失败,我猜是因为 Date 对象在方法的两个之间是 (运行domly) 相同的电话。 有没有更好的方法来测试调用多个方法的顺序?

p.s. 这在 Android SDK org.mockito.Mockito.inOrder

中很容易完成

您可以使用 String 来跟踪订单:

class ChildClass: MyClass {
    var order = ""

    override func methodA() {
        super.methodA()
        order = String((order + "A").suffix(3))
    }
    override func methodB() {
        super.methodB()
        order = String((order + "B").suffix(3))
    }
    override func methodC() {
        super.methodC()
        order = String((order + "C").suffix(3))
    }
}

然后,只需检查 order 是否为 "ABC"


或者,如果在AC之间多次调用B有效:

class ChildClass: MyClass {
    var order = ""

    override func methodA() {
        super.methodA()
        order = order.replacingOccurrences(of: "A", with: "") + "A"
    }
    override func methodB() {
        super.methodB()
        order = order.replacingOccurrences(of: "B", with: "") + "B"
    }
    override func methodC() {
        super.methodC()
        order = order.replacingOccurrences(of: "C", with: "") + "C"
    }
}

示例:

let c = ChildClass()
c.methodA()
c.methodB()
c.methodB()
c.methodC()

print(c.order)
ABC

我很喜欢使用 XCTestExpectation 来处理这种事情。这是一个选项。

class MyTestableClass: MyClass {
    var methodAHandler: (() -> Void)?
    // ...

    override func methodA() {
        methodAHandler?()  
        super.methodA()
    }

然后在你的测试用例中

   let expA = XCTestExpectation(description: "Method A Called")
   let expB = ...
   let expo = ...

   objectUnderTest.methodAHandler = { expA.fulfill() }
   /// ...

   objectUnderTest.myMethod()

   // ensure you use the enforceOrder param, which is optional
   wait(for: [expA, expB, expC], timeout: 1.0, enforceOrder: true)

XCTestExpectation更多是为了异步测试,所以等待有点搞笑。但是,它确实可以满足您的需求,即使 myMethod 的内部最终由于某种原因变得异步,它也会继续工作。

虽然我自己没有使用过,但您也可能想看看 Cuckoo。这是 Swift.

的模拟框架

你问的问题不对。从单元测试的角度来看,您不应该 know/care 被测试的方法调用其他方法,或者即使存在其他方法。

单元测试应该验证测试方法的一些可观察结果。被测方法内部发生的任何事情与单元测试的上下文无关。

这是因为单元测试应该验证单元的行为是否符合预期,即它们应该根据规范进行验证,而不是根据实现进行验证。

让我们考虑一个简单的例子,对 isPrime(n) 函数进行单元测试。除非您正在进行性能测试,否则您只关心函数 returns 是否是几个数字的适当结果。您不关心该函数是否检查所有可能的除数,或者它是否使用所有已知素数的数据库,或者是否将素数检查委托给某个第 3 方 library/service.

情况和你的差不多。这三个方法按特定顺序调用的事实需要通过被测单元的外部接口进行验证。例如,如果这三个方法进行 API 调用,则模拟 API 客户端并期望它被请求三次,并且具有预期的 URL/payload。如果调用这三个方法不会导致任何明显的变化,那么从一开始就没有太多可以测试的,所以再次以特定顺序调用三个方法这一事实变得无关紧要。

单元测试是关于验证该单元的执行结果,仅此而已。现在,在命令式编程语言中,input->output 函数是少数,但这并不意味着我们不能间接测试函数是否按预期运行。您可以使用模拟,或在函数执行后验证对象的某些属性。同样,如果没有办法从外部检查方法的顺序,那么您就没有要验证的规范。

首先,制作一个记录订单的模拟对象。没有日期,没有字符串。只是一个枚举。

class MockMyClass: MyClass {
    enum invocation {
        case methodA
        case methodB
        case methodC
    }
    private var invocations: [invocation] = []

    override func methodA() {
        invocations.append(.methodA)
    }

    override func methodB() {
        invocations.append(.methodB)
    }

    override func methodC() {
        invocations.append(.methodC)
    }

    func verify(expectedInvocations: [invocation], file: StaticString = #file, line: UInt = #line) {
        if invocations != expectedInvocations {
            XCTFail("Expected \(expectedInvocations), but got \(invocations)", file: file, line: line)
        }
    }
}

然后在测试中:

mock.verify(expectedInvocations: [.methodA, .methodB, .methodC])

没有异步等待。调用简单。清除失败消息。