如何验证使用 XCTAssert 调用了 class 方法?

How can I verify a class method is called using XCTAssert?

我有一项服务class,我想声明两件事

  1. 调用了一个方法
  2. 传递给该方法的参数正确

这是我的 class

protocol OAuthServiceProtocol {
    func initAuthCodeFlow() -> Void
     func renderOAuthWebView(forService service: IdentityEndpoint, queryitems: [String: String]) -> Void
}

class OAuthService: OAuthServiceProtocol {

    fileprivate let apiClient: APIClient

    init(apiClient: APIClient) {
        self.apiClient = apiClient
    }

    func initAuthCodeFlow() -> Void {

    }

    func renderOAuthWebView(forService service: IdentityEndpoint, queryitems: [String: String]) -> Void {

    }
}

这是我的测试

class OAuthServiceTests: XCTestCase {
    var mockAPIClient: APIClient!
    var mockURLSession: MockURLSession!
    var sut: OAuthService!

    override func setUp() {
        mockAPIClient = APIClient()
        mockAPIClient.session = MockURLSession(data: nil, urlResponse: nil, error: nil)
        sut = OAuthService(apiClient: mockAPIClient)
    }

    func test_InitAuthCodeFlow_CallsRenderOAuthWebView() {
        let renderOAuthWebViewExpectation = expectation(description: "RenderOAuthWebView")

        class OAuthServiceMock: OAuthService {
            override func initAuthCodeFlow() -> Void {

            }

            override func renderOAuthWebView(forService service: IdentityEndpoint, queryitems: [String: String]) {
                renderOAuthWebViewExpectation.fulfill()
            }
        }
    }
}

我希望创建 OAuthService 的本地子 class,将其分配为我的 sut 并调用类似 sut.initAuthCodeFlow() 的内容,然后断言我的期望已实现。

我相信这应该满足第 1 点。但是,当我尝试将其分配为已实现时,我无法访问我的期望,因为我收到以下错误

Class declaration cannot close over value 'renderOAuthWebViewExpectation' defined in outer scope

如何将其标记为已完成?

我正在遵循 TDD 方法,所以我知道我的 OAuthService 无论如何都会在此时产生失败的测试*

在您的模拟上创建一个 属性,在您希望调用的方法中改变它的值。然后您可以使用您的 XCTAssertEqual 检查道具是否已更新。

   func test_InitAuthCodeFlow_CallsRenderOAuthWebView() {
        let renderOAuthWebViewExpectation = expectation(description: "RenderOAuthWebView")

        class OAuthServiceMock: OAuthService {
            var renderOAuthWebViewExpectation: XCTestExpectation!
            var didCallRenderOAuthWebView = false

            override func renderOAuthWebView(forService service: IdentityEndpoint, queryitems: [String: String]) {
                didCallRenderOAuthWebView = true
                renderOAuthWebViewExpectation.fulfill()
            }
        }

        let sut = OAuthServiceMock(apiClient: mockAPIClient)

        XCTAssertEqual(sut.didCallRenderOAuthWebView, false)
        sut.renderOAuthWebViewExpectation = renderOAuthWebViewExpectation

        sut.initAuthCodeFlow()
        waitForExpectations(timeout: 1) { _ in
            XCTAssertEqual(sut.didCallRenderOAuthWebView, true)
        }

    }

I was hoping to create a local sub class of OAuthService, assign that as my sut and call something like like sut.initAuthCodeFlow() and then assert that my expectation was fulfilled.

我强烈建议您不要使用这种方法。如果您的 SUT 是子类的一个实例,那么您的测试并不是真正的测试 OAuthService,而是 OAuthService 模拟。

此外,如果我们将测试视为一种工具:

  • 防止代码更改时出现错误
  • 帮助重构和维护代码

那么我会争辩说,测试调用某个函数调用另一个函数并不是一个好的测试。我知道这很苛刻,所以让我来解释一下为什么会这样。

它唯一测试的是 initAuthCodeFlow() 在幕后调用 renderOAuthWebView(forService:, queryitems:)。它对被测系统的实际 行为 没有任何断言,无论是否直接产生输出。如果我要编辑 renderOAuthWebView(forService:, queryitems:) 的实现并添加一些会在 运行 时崩溃的代码,则此测试不会失败。

这样的测试无助于保持代码库易于更改,因为如果您想更改 OAuthService 的实现,可能通过向 renderOAuthWebView(forService:, queryitems:) 添加参数或重命名queryitemsqueryItems 以匹配大小写,您必须更新生产代码和测试代码。换句话说,测试将妨碍您进行重构 - 改变代码的外观而不改变代码的行为方式 - 没有任何额外的好处。

那么,应该如何测试 OAuthService 以防止错误并帮助快速移动?诀窍在于测试行为而不是实现。

应该OAuthService做什么initAuthCodeFlow() 没有 return 任何值,所以我们可以检查直接输出,但我们仍然可以检查间接输出,副作用。

我在这里猜测,但我从你的测试中检查 renderOAuthWebView(forService:, queryitems:) 我会和它得到一个 APIClient 类型作为输入的事实我会说它会为某个 URL 呈现某种网络视图,然后向给定的 APIClient 发出另一个请求,可能使用从网络视图接收到的 OAuth 令牌?

测试与 APIClient 交互的一种方法是对要调用的预期端点进行断言。您可以使用 OHHTTPStubs 之类的工具或使用 URLSession 的自定义测试替身来记录它收到的请求并允许您检查它们。

对于webview的呈现,可以使用delegate模式,设置一个符合delegate协议的test double记录是否被调用。或者您可以在更高级别进行测试并检查 UIWindow 其中测试 运行 查看根视图控制器是否是具有 Web 视图的控制器。

归根结底,一切都是权衡取舍的问题。您采用的方法并没有错,它只是针对断言代码实现而不是其行为进行了更多优化。我希望通过这个答案,我展示了一种不同类型的优化,一种偏向于行为的优化。以我的经验,这种测试方式在中长期 运行.

中证明更有帮助