tl; dr Первоначально запрошенный ответ приходит к концу
На основании вашего описания я не думаю, что вам нужно использовать DispatchQueue.main
в этом разделе рабочего кода.Поскольку клиент loginUser(from:with:and:completionHandler:)
делает что-то асинхронное, а затем вызывает обработчик завершения, он может гарантировать, что обработчик завершения вызывается в главном потоке.И это произойдет после того, как ответ был декодирован в фоновом режиме.
Если это так, то в контроллере представления onLoginButtonTapped(_)
нет необходимости в обработчике завершения для повторной отправки восновная очередь.Мы уже знаем, что он работает в главной очереди, поэтому он может просто вызывать self.presentAlertVC()
без каких-либо уловок.
Это подводит нас к тестовому коду.Ваш фальшивый loginUser
не должен ничего планировать на DispatchQueue.main
.Реальная версия делает, но подделка не должна.Мы можем устранить это, сделав тестовый код еще проще.Весь тестовый код может быть синхронным, что устраняет необходимость использования XCTestExpectation.
Теперь ваш поддельный клиент не должен вызывать обработчик завершения с жестко заданными значениями.Мы хотим, чтобы каждый тест мог настроить то, что он хочет.Это позволит вам проверить каждый путь.Если вы еще не делаете подделку с использованием протокола, давайте представим один:
protocol ClientProtocol {
func loginUser(from url: URL,
with username: String,
and password: String,
completionHandler: @escaping (DrivetimeUserProfile?, DrivetimeAPIError.LoginError?) -> Void)
}
Возможно, вы уже делаете это.Если нет, то вы создаете тестовый подкласс вашего клиента прямо сейчас.Иди с протоколом.Итак, в вашем контроллере представления у вас будет несколько розеток и этот клиент:
@IBOutlet private(set) var loginButton: UIButton!
@IBOutlet private(set) var usernameTextField: UITextField!
@IBOutlet private(set) var passwordTextField: UITextField!
var client: ClientProtocol = Client()
Сделайте розетки private(set)
вместо private
, чтобы тесты могли получить к ним доступ.
Вот тесты, которые я бы написал.Терпи меня, я в конце концов доберусь до того, о котором ты спрашивал.Сначала давайте проверим, что розетки настроены.Для моего примера я предполагаю, что вы используете контроллер представлений на основе раскадровки.
final class ViewControllerTests: XCTestCase {
private var sut: ViewController!
override func setUp() {
super.setUp()
let storyboard = UIStoryboard(name: "Main", bundle: nil)
sut = storyboard.instantiateViewController(withIdentifier: "ViewController") as? ViewController
}
override func tearDown() {
sut = nil
super.tearDown()
}
func test_outlets_shouldBeConnected() {
sut.loadViewIfNeeded()
XCTAssertNotNil(sut.loginButton, "loginButton")
XCTAssertNotNil(sut.usernameTextField, "usernameTextField")
XCTAssertNotNil(sut.passwordTextField, "passwordTextField")
}
}
Далее я хочу протестировать то, что вы не упомянули: это, когда пользователь нажимает на логинКнопка вызывает loginUser
, передавая ожидаемые параметры.Для этого мы можем передать фиктивный объект, который позволяет нам проверить, как вызывается loginUser
.Это собирается захватить количество вызовов и все параметры.У него есть метод проверки для подтверждения большинства параметров.Отдельный метод дает тестам способ вызова обработчика завершения.
private class MockClient: ClientProtocol {
private var loginUserCallCount = 0
private var loginUserArgsURL: [URL] = []
private var loginUserArgsUsername: [String] = []
private var loginUserArgsPassword: [String] = []
private var loginUserArgsCompletionHandler: [(DrivetimeUserProfile?, DrivetimeAPIError.LoginError?) -> Void] = []
func loginUser(from url: URL,
with username: String,
and password: String,
completionHandler: @escaping (DrivetimeUserProfile?, DrivetimeAPIError.LoginError?) -> Void) {
loginUserCallCount += 1
loginUserArgsURL.append(url)
loginUserArgsUsername.append(username)
loginUserArgsPassword.append(password)
loginUserArgsCompletionHandler.append(completionHandler)
}
func verifyLoginUser(from url: URL,
with username: String,
and password: String,
file: StaticString = #file,
line: UInt = #line) {
XCTAssertEqual(loginUserCallCount, 1, "call count", file: file, line: line)
XCTAssertEqual(url, loginUserArgsURL.first, "url", file: file, line: line)
XCTAssertEqual(username, loginUserArgsUsername.first, "username", file: file, line: line)
XCTAssertEqual(password, loginUserArgsPassword.first, "password", file: file, line: line)
}
func invokeLoginUserCompletionHandler(profile: DrivetimeUserProfile?,
error: DrivetimeAPIError.LoginError?,
file: StaticString = #file,
line: UInt = #line) {
guard let handler = loginUserArgsCompletionHandler.first else {
XCTFail("No loginUser completion handler captured", file: file, line: line)
return
}
handler(profile, error)
}
}
Поскольку мы хотим использовать это для нескольких тестов, давайте поместим его в тестовое приспособление:
private var sut: ViewController!
private var mockClient: MockClient! // ?
override func setUp() {
super.setUp()
let storyboard = UIStoryboard(name: "Main", bundle: nil)
sut = storyboard.instantiateViewController(withIdentifier: "ViewController") as? ViewController
mockClient = MockClient() // ?
sut.client = mockClient // ?
}
override func tearDown() {
sut = nil
mockClient = nil // ?
super.tearDown()
}
СейчасЯ готов написать первый тест, при котором нажатие кнопки входа вызывает клиента:
func test_tappingLoginButton_shouldLoginUserWithEnteredUsernameAndPassword() {
sut.loadViewIfNeeded()
sut.usernameTextField.text = "USER"
sut.passwordTextField.text = "PASS"
sut.loginButton.sendActions(for: .touchUpInside)
mockClient.verifyLoginUser(from: URL(string: "https://your.url")!, with: "USER", and: "PASS")
}
Обратите внимание, что тест не вызывает sut.onLoginButtonTapped(sut.loginButton)
.Вместо этого тест сообщает кнопке входа в систему обработать .touchUpInside
.Название метода действия не имеет значения, поэтому IBAction может быть объявлено private
.
Наконец, мы подошли к вашему первоначальному вопросу.Если какой-либо результат передан обработчику завершения, отображается ли предупреждение?Для этого мы можем использовать мою библиотеку MockUIAlertController .Добавьте это к своей цели теста.Он написан на Objective-C, поэтому создайте соединительный заголовок, который импортирует MockUIAlertController.h
.
Верификатор предупреждений собирает информацию о представленном предупреждении, фактически не представляя никаких предупреждений.Самый безопасный способ убедиться, что никакие фактические предупреждения не инициируются модульными тестами, - это добавить его к тестовому устройству:
private var sut: ViewController!
private var mockClient: MockClient!
private var alertVerifier: QCOMockAlertVerifier! // ?
override func setUp() {
super.setUp()
let storyboard = UIStoryboard(name: "Main", bundle: nil)
sut = storyboard.instantiateViewController(withIdentifier: "ViewController") as? ViewController
mockClient = MockClient()
sut.client = mockClient
alertVerifier = QCOMockAlertVerifier() // ?
}
override func tearDown() {
sut = nil
mockClient = nil
alertVerifier = nil // ?
super.tearDown()
}
С безопасными перехватами предупреждений теперь мы можем написать тест, который вы хотели в первую очередь:
func test_invokeLoginCompletionHandler_withEmptyData_shouldPresentAlert() {
sut.loadViewIfNeeded()
sut.loginButton.sendActions(for: .touchUpInside)
mockClient.invokeLoginUserCompletionHandler(profile: nil, error: .EmptyData)
XCTAssertEqual(alertVerifier.presentedCount, 1, "presented count")
// more assertions here...
}
Поскольку мы создали достаточно инфраструктуры, тестовый код прост. QCOMockAlertVerifier имеет много других свойств , которые вы можете проверить.Вы также можете заставить его выполнять действия кнопки.
С этим тестом легко писать другие.Вы можете вызвать обработчик завершения с другим случаем ошибки или с действительным профилем.Асинхронное тестирование не требуется.
Если вам действительно нужно явно запланировать предупреждение в главном потоке, а не вызывать его напрямую, мы можем это сделать.Создайте ожидание и попросите его проверить его.Добавьте короткое ожидание перед утверждением.
func test_invokeLoginCompletionHandler_withEmptyData_shouldPresentAlert() {
sut.loadViewIfNeeded()
sut.loginButton.sendActions(for: .touchUpInside)
let expectation = self.expectation(description: "alert presented") // ?
alertVerifier.completion = { expectation.fulfill() } // ?
mockClient.invokeLoginUserCompletionHandler(profile: nil, error: .EmptyData)
waitForExpectations(timeout: 0.001) // ?
XCTAssertEqual(alertVerifier.presentedCount, 1, "presented count")
// more assertions here...
}
(Подобные вещи будут подробно рассмотрены в книге по модульному тестированию iOS, которую я сейчас пишу .)