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

Posted

技术标签:

【中文标题】如何验证使用 XCTAssert 调用了类方法?【英文标题】:How can I verify a class method is called using XCTAssert? 【发布时间】:2019-01-01 08:54:16 【问题描述】:

我有一个服务类,我想断言 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 

    

这是我的测试

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 的本地子类,将其指定为我的 sut 并调用类似 sut.initAuthCodeFlow() 之类的名称,然后断言我的期望已实现。

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

类声明不能关闭值 在外部范围中定义的“renderOAuthWebViewExpectation”

如何将其标记为已完成?

我采用的是 TDD 方法,所以我知道我的 OAuthService 在这一点上无论如何都会产生一个失败的测试*

【问题讨论】:

【参考方案1】:

我希望创建一个 OAuthService 的本地子类,将其指定为我的 sut 并调用类似 sut.initAuthCodeFlow() 之类的东西,然后断言我的期望已经实现。

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

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

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

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

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

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

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

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

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

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

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

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

【讨论】:

【参考方案2】:

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

   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)
        

    

【讨论】:

以上是关于如何验证使用 XCTAssert 调用了类方法?的主要内容,如果未能解决你的问题,请参考以下文章

XCTAssert/XCTFail 在异步回调中是不是安全?

TP5不安装composer使用验证码

什么体现了类的多态性?

python之类的相关名词解释

IOS为啥对一个类调用ViewController进行单元测试

在c++中创建了类,但在创建新对象时,编译显示:不存在默认构造函数