Swift-使用XCTest测试包含闭包的函数



我是Swift的新手,目前正在尝试编写一个单元测试(使用XCTest)来测试以下功能:

func login(email: String, password: String)  {
Auth.auth().signIn(withEmail: email, password: password) { (user, error) in
if let _error = error {
print(_error.localizedDescription)
} else {
self.performSegue(identifier: "loginSeg")
}
}
}

我的研究表明,我需要使用XCTestExpection功能,因为XCTest默认情况下是同步执行的,这意味着它不会等待关闭完成运行(如果我错了,请纠正我)。

让我感到困惑的是如何测试登录函数,因为它本身调用异步函数Auth.auth().signIn()。我正在尝试测试登录是否成功。

如果这个问题已经得到回答,我很抱歉,但我找不到直接解决这个问题的答案。

感谢

更新:

在答案和进一步研究的帮助下,我通过登录功能修改为使用转义闭包:

func login(email: String, password: String, completion: @escaping(Bool)->())  {
Auth.auth().signIn(withEmail: email, password: password) { (user, error) in
if let _error = error {
print(_error.localizedDescription)
completion(false)
} else {
self.performSegue(identifier: "loginSeg")
completion(true)
}
}
}

然后我以以下方式进行测试:

func testLoginSuccess() {
// other setup
let exp = expectation(description: "Check Login is successful")
let result = login.login(email: email, password: password) { (loginRes) in
loginResult = loginRes
exp.fulfill()
}
waitForExpectations(timeout: 10) { error in
if let error = error {
XCTFail("waitForExpectationsWithTimeout errored: (error)")
}
XCTAssertEqual(loginResult, true)
}
}

我的测试功能现在成功地测试了登录功能。

希望这能帮助到别人,因为它让我困惑了一段时间:)

对Auth的调用是一个体系结构边界。如果单元测试达到这样的界限,那么它们会更快、更可靠,但不要越过它们。我们可以通过在协议后面隔离Auth单例来实现这一点。

我猜是signIn的签名。不管它是什么,复制并粘贴到一个协议中:

protocol AuthProtocol {
func signIn(withEmail email: String, password: String, completion: @escaping (String, NSError?) -> Void)
}

这相当于完整Auth接口的一个薄片,只包含您想要的部分。这是接口分离原则的一个例子。

然后扩展Auth以符合此协议。它已经这样做了,所以一致性是空的。

extension Auth: AuthProtocol {}

现在在视图控制器中,将对Auth.auth()的直接调用提取到具有默认值的属性中:

var auth: AuthProtocol = Auth.auth()

与此属性对话,而不是直接与Auth.auth():对话

auth.signIn(withEmail: email, …etc…

这引入了一个Seam。一个测试可以用一个test Spy实现来代替auth,记录signIn是如何被调用的。

final class SpyAuth: AuthProtocol {
private(set) var signInCallCount = 0
private(set) var signInArgsEmail: [String] = []
private(set) var signInArgsPassword: [String] = []
private(set) var signInArgsCompletion: [(String, Foundation.NSError?) -> Void] = []
func signIn(withEmail email: String, password: String, completion: @escaping (String, Foundation.NSError?) -> Void) {
signInCallCount += 1
signInArgsEmail.append(email)
signInArgsPassword.append(password)
signInArgsCompletion.append(completion)
}
}

测试可以将SpyAuth注入到视图控制器中,拦截通常会进入Auth的所有内容。正如您所看到的,这包括完成闭包。我会写

  • 一个测试来确认调用计数和非闭包参数
  • 另一个测试是获取捕获的闭包并称之为成功
  • 如果您的代码没有print(_)语句,我也会称之为failure

最后,还有赛格的问题。苹果没有给我们任何方法来对它们进行单元测试。作为一种变通方法,您可以制作一个部分mock。类似这样的东西:

final class TestableLoginViewController: LoginViewController {
private(set) var performSegueCallCount = 0
private(set) var performSegueArgsIdentifier: [String] = []
private(set) var performSegueArgsSender: [Any?] = []
override func performSegue(withIdentifier identifier: String, sender: Any?) {
performSegueCallCount += 1
performSegueArgsIdentifier.append(identifier)
performSegueArgsSender.append(sender)
}
}

这样,您就可以拦截对performSegue的调用。这并不理想,因为这是一种遗留的代码技术。但它应该让你开始。

final class LoginViewControllerTests: XCTestCase {
private var sut: TestableLoginViewController!
private var spyAuth: SpyAuth!
override func setUp() {
super.setUp()
sut = TestableLoginViewController()
spyAuth = SpyAuth()
sut.auth = spyAuth
}
override func tearDown() {
sut = nil
spyAuth = nil
super.tearDown()
}
func test_login_shouldCallAuthSignIn() {
sut.login(email: "EMAIL", password: "PASSWORD")

XCTAssertEqual(spyAuth.signInCallCount, 1, "call count")
XCTAssertEqual(spyAuth.signInArgsEmail.first, "EMAIL", "email")
XCTAssertEqual(spyAuth.signInArgsPassword.first, "PASSWORD", "password")
}
func test_login_withSuccess_shouldPerformSegue() {
sut.login(email: "EMAIL", password: "PASSWORD")
let completion = spyAuth.signInArgsCompletion.first

completion?("DUMMY", nil)

XCTAssertEqual(sut.performSegueCallCount, 1, "call count")
XCTAssertEqual(sut.performSegueArgsIdentifier.first, "loginSeg", "identifier")
let sender = sut.performSegueArgsSender.first
XCTAssertTrue(sender as? TestableLoginViewController === sut,
"Expected sender (sut!), but was (String(describing: sender))")
}
}

这里绝对没有异步,所以没有waitForExpectations。我们捕获了闭包,我们称之为闭包。

Jon的回答很好,我还不能添加评论,所以我会在这里添加我的建议。对于那些(出于任何原因)拥有静态/类函数而不是singleton或实例函数的人,这可能会对您有所帮助:

例如,如果您有Auth.signIn(withEmail: emai...,其中signIn是一个静态函数。代替使用:

var auth: AuthProtocol = Auth.auth()

用途:

var auth: AuthProtocol.Type = Auth.self

并像这样分配

sut.auth = SpyAuth.self

最新更新