如何验证使用 XCTAssert 调用了 class 方法?
How can I verify a class method is called using XCTAssert?
我有一项服务class,我想声明两件事
- 调用了一个方法
- 传递给该方法的参数正确
这是我的 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:)
添加参数或重命名queryitems
到 queryItems
以匹配大小写,您必须更新生产代码和测试代码。换句话说,测试将妨碍您进行重构 - 改变代码的外观而不改变代码的行为方式 - 没有任何额外的好处。
那么,应该如何测试 OAuthService
以防止错误并帮助快速移动?诀窍在于测试行为而不是实现。
应该OAuthService
做什么? initAuthCodeFlow()
没有 return 任何值,所以我们可以检查直接输出,但我们仍然可以检查间接输出,副作用。
我在这里猜测,但我从你的测试中检查 renderOAuthWebView(forService:, queryitems:)
我会和它得到一个 APIClient
类型作为输入的事实我会说它会为某个 URL 呈现某种网络视图,然后向给定的 APIClient
发出另一个请求,可能使用从网络视图接收到的 OAuth 令牌?
测试与 APIClient
交互的一种方法是对要调用的预期端点进行断言。您可以使用 OHHTTPStubs 之类的工具或使用 URLSession
的自定义测试替身来记录它收到的请求并允许您检查它们。
对于webview的呈现,可以使用delegate模式,设置一个符合delegate协议的test double记录是否被调用。或者您可以在更高级别进行测试并检查 UIWindow
其中测试 运行 查看根视图控制器是否是具有 Web 视图的控制器。
归根结底,一切都是权衡取舍的问题。您采用的方法并没有错,它只是针对断言代码实现而不是其行为进行了更多优化。我希望通过这个答案,我展示了一种不同类型的优化,一种偏向于行为的优化。以我的经验,这种测试方式在中长期 运行.
中证明更有帮助
我有一项服务class,我想声明两件事
- 调用了一个方法
- 传递给该方法的参数正确
这是我的 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 likesut.initAuthCodeFlow()
and then assert that my expectation was fulfilled.
我强烈建议您不要使用这种方法。如果您的 SUT 是子类的一个实例,那么您的测试并不是真正的测试 OAuthService
,而是 OAuthService
模拟。
此外,如果我们将测试视为一种工具:
- 防止代码更改时出现错误
- 帮助重构和维护代码
那么我会争辩说,测试调用某个函数调用另一个函数并不是一个好的测试。我知道这很苛刻,所以让我来解释一下为什么会这样。
它唯一测试的是 initAuthCodeFlow()
在幕后调用 renderOAuthWebView(forService:, queryitems:)
。它对被测系统的实际 行为 没有任何断言,无论是否直接产生输出。如果我要编辑 renderOAuthWebView(forService:, queryitems:)
的实现并添加一些会在 运行 时崩溃的代码,则此测试不会失败。
这样的测试无助于保持代码库易于更改,因为如果您想更改 OAuthService
的实现,可能通过向 renderOAuthWebView(forService:, queryitems:)
添加参数或重命名queryitems
到 queryItems
以匹配大小写,您必须更新生产代码和测试代码。换句话说,测试将妨碍您进行重构 - 改变代码的外观而不改变代码的行为方式 - 没有任何额外的好处。
那么,应该如何测试 OAuthService
以防止错误并帮助快速移动?诀窍在于测试行为而不是实现。
应该OAuthService
做什么? initAuthCodeFlow()
没有 return 任何值,所以我们可以检查直接输出,但我们仍然可以检查间接输出,副作用。
我在这里猜测,但我从你的测试中检查 renderOAuthWebView(forService:, queryitems:)
我会和它得到一个 APIClient
类型作为输入的事实我会说它会为某个 URL 呈现某种网络视图,然后向给定的 APIClient
发出另一个请求,可能使用从网络视图接收到的 OAuth 令牌?
测试与 APIClient
交互的一种方法是对要调用的预期端点进行断言。您可以使用 OHHTTPStubs 之类的工具或使用 URLSession
的自定义测试替身来记录它收到的请求并允许您检查它们。
对于webview的呈现,可以使用delegate模式,设置一个符合delegate协议的test double记录是否被调用。或者您可以在更高级别进行测试并检查 UIWindow
其中测试 运行 查看根视图控制器是否是具有 Web 视图的控制器。
归根结底,一切都是权衡取舍的问题。您采用的方法并没有错,它只是针对断言代码实现而不是其行为进行了更多优化。我希望通过这个答案,我展示了一种不同类型的优化,一种偏向于行为的优化。以我的经验,这种测试方式在中长期 运行.
中证明更有帮助