Звонок в Auth является архитектурной границей.Модульные тесты быстрее и надежнее, если они доходят до таких границ, но не пересекают их.Мы можем сделать это, изолируя синглтон Auth за протоколом.
Я предполагаю, что подпись signIn
.Что бы это ни было, скопируйте и вставьте его в протокол:
protocol AuthProtocol {
func signIn(withEmail email: String, password: String, completion: @escaping (String, NSError?) -> Void)
}
Это действует как тонкий фрагмент полного интерфейса аутентификации, занимая только ту часть, которую вы хотите.Это пример принципа разделения интерфейса.
Затем расширьте Auth, чтобы соответствовать этому протоколу.Это уже происходит, поэтому соответствие пусто.
extension Auth: AuthProtocol {}
Теперь в вашем контроллере представления извлеките прямой вызов Auth.auth()
в свойство со значением по умолчанию:
var auth: AuthProtocol = Auth.auth()
Поговорите с этим свойством, а не напрямую с Auth.auth()
:
auth.signIn(withEmail: email, …etc…
Это вводит шов.Тест может заменить 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 в контроллер представления, перехватывая все, что обычно происходитк авторизацииКак видите, это включает в себя завершение завершения.Я бы написал
- Один тест для подтверждения количества вызовов и аргументов незамкнутости
- Другой тест для получения захваченного замыкания и успешного его вызова.
- Я также назвал бы это с ошибкой, если бы в вашем коде не было оператора
print(_)
.
Наконец, есть проблема с segues.Apple не дала нам никакого способа их модульного тестирования.В качестве обходного пути, вы можете сделать частичный макет.Примерно так:
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
.Мы фиксируем закрытие, мы называем закрытие.
(Подобные вещи будут подробно рассмотрены в книге по тестированию модулей iOS, которую я сейчас пишу .)