如何在 struct/class 中存根 Swift "Trait/Mixin" 方法进行测试

How to stub Swift "Trait/Mixin" method in struct/class for testing

我最近了解到如何通过创建协议并使用默认实现扩展该协议来将 "Traits/Mixins" 添加到 Swift 中的 struct/class。这很棒,因为它允许我添加功能来查看控制器,而不必向所述视图控制器添加一堆帮助对象。我的问题是,如何存根这些默认实现提供的调用?

这是一个简单的例子:

protocol CodeCop {
  func shouldAllowExecution() -> Bool
}

extension CodeCop {
  func shouldAllowExecution() -> Bool {
    return arc4random_uniform(2) == 0
  }
}

struct Worker : CodeCop {
  func doSomeStuff() -> String {
    if shouldAllowExecution() {
       return "Cop allowed it"
     } else {
       return "Cop said no"
    }
  }
}

如果我想写两个测试,一个验证 String "Cop allowed it" 在 CodeCop 不允许执行时由 doStuff() 返回,另一个测试验证 String "Cop said no"当 CodeCop 不允许执行时由 doStuff() 返回。

我不确定这是否是您正在寻找的,但是您可以 测试此行为而不更新您的代码 的一种方法是通过以下方式更新您的项目结构:

  • CodeCop 协议保存在一个文件中(比方说 CodeCop.swift)并将扩展代码添加到另一个文件中(CodeCop+shouldAllowExecution.swift)

  • 虽然 CodeCop.swift 链接到您的主要目标和测试目标,但 CodeCop+shouldAllowExecution.swift 仅在主要目标中。

  • 创建一个测试文件 CodeCopTest.swift,仅在测试目标中可用,其中包含 shouldAllowExecution 的另一个默认实现,这将帮助您 运行 您的测试。

这是一个潜在的 CodeCopTest.swift 文件

import XCTest

fileprivate var shouldCopAllowExecution: Bool = false

fileprivate extension CodeCop {

    func shouldAllowExecution() -> Bool {
        return shouldCopAllowExecution
    }
}

class PeopleListDataProviderTests: XCTestCase {

    var codeCop: CodeCop!

    override func setUp() {
        super.setUp()
        codeCop = CodeCop()
    }

    override func tearDown() {
        codeCop = nil
        super.tearDown()
    }

    func testWhenCopAllows() {
        shouldCopAllowExecution = true
        XCTAssertEqual(codeCop.doSomeStuff(), "Cop allowed it", "Cop should say 'Cop allowed it' when he allows execution")
    }

    func testWhenCopDenies() {
        shouldCopAllowExecution = false
        XCTAssertEqual(codeCop.doSomeStuff(), "Cop said no", "Cop should say 'Cop said no' when he does not allow execution")
    }
}

这很简单,只需在您的测试目标中编写一个名为 CodeCopStub 的附加协议即可,该协议继承自 CodeCop:

protocol CodeCopStub: CodeCop {
    // CodeCopStub declares a static value on the implementing type
    // that you can use to control what is returned by
    // `shouldAllowExecution()`.
    //
    // Note that this has to be static, because you can't add stored instance
    // variables in extensions.
    static var allowed: Bool { get }
}

然后扩展 CodeCopStubshouldAllowExecution() 方法(继承自 CodeCop)到 return 一个取决于新静态变量 allowed 的值。这将覆盖任何实现 CodeCopStub.

的类型的原始 CodeCop 实现
extension CodeCopStub {
    func shouldAllowExecution() -> Bool {
        // We use `Self` here to refer to the implementing type (`Worker` in
        // this case).
        return Self.allowed
    }
}

此时你剩下要做的就是让 Worker 符合 CodeCopStub:

extension Worker: CodeCopStub {
    // It doesn't matter what the initial value of this variable is, because
    // you're going to set it in every test, but it has to have one because
    // it's static.
    static var allowed: Bool = false
}

您的测试将如下所示:

func testAllowed() {
    // Create the worker.
    let worker = Worker()
    // Because `Worker` has been extended to conform to `CodeCopStub`, it will
    // have this static property. Set it to true to cause
    // `shouldAllowExecution()` to return `true`.
    Worker.allowed = true

    // Call the method and get the result.
    let actualResult = worker.doSomeStuff()
    // Make sure the result was correct.
    let expectedResult = "Cop allowed it"
    XCTAssertEqual(expectedResult, actualResult)
}

func testNotAllowed() {
    // Same stuff as last time...
    let worker = Worker()
    // ...but you tell it not to allow it.
    Worker.allowed = false

    let actualResult = worker.doSomeStuff()
    // This time, the expected result is different.
    let expectedResult = "Cop said no"
    XCTAssertEqual(expectedResult, actualResult)
}

请记住,所有这些代码都应该放在您的测试目标中,而不是您的主要目标。通过把它放在你的测试目标中,它的 none 会影响你的原始代码,并且不需要对原始代码进行修改。