如何使用 XCTAssert 验证调用类方法?

Har*_*lue 1 tdd unit-testing swift xctestcase

我有一个服务类,我想断言两件事

  1. 一个方法被称为
  2. 正确的参数传递给该方法

这是我的班级

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 {

    }
}
Run Code Online (Sandbox Code Playgroud)

这是我的测试

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()
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

类声明无法关闭外部范围中定义的值“renderOAuthWebViewExpectation”

我如何将其标记为已完成?

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

mok*_*gio 5

我希望创建 的本地子类OAuthService,将其指定为我的 sut 并调用类似的内容sut.initAuthCodeFlow(),然后断言我的期望得到满足。

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

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

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

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

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

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

那么,应该如何OAuthService以一种可以防止错误并有助于快速发展的方式进行测试呢?诀窍在于测试行为而不是实现。

OAuthService 该做什么initAuthCodeFlow()不返回任何值,因此我们可以检查直接输出,但我们仍然可以检查间接输出、副作用。

我在这里做出一个猜测,但我从你的测试中检查renderOAuthWebView(forService:, queryitems:)我是否会获得一个APIClient类型作为输入,我想说它会为某个 URL 呈现某种 Web 视图,然后创建另一个APIClient是否可以使用从 Web 视图收到的 OAuth 令牌向给定的请求?

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

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

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