如何验证使用 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 在异步回调中是不是安全?