Swift - использование XCTest для тестирования функции, содержащей замыкание - PullRequest
0 голосов
/ 31 января 2019

Я довольно новичок в 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")
        }
    }
}

Мои исследования показали, что мне нужно использовать функциональность XCTestExpectation как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)
    }
}

Моя тестовая функция теперь успешно проверяет функциональность входа в систему.

Надеюсь, это кому-нибудь поможет, так как оставило меня в тупике на некоторое время:)

Ответы [ 2 ]

0 голосов
/ 21 июня 2019

Ответ Джона отличный, я пока не могу добавлять комментарии, поэтому я добавлю свой совет здесь.Для тех, у кого (по любой причине) есть статическая / классовая функция вместо синглтона или функция экземпляра, это может помочь вам:

Например, если у вас есть Auth.signIn(withEmail: emai... гдеsignIn - статическая функция.Вместо использования:

var auth: AuthProtocol = Auth.auth()

Использование:

var auth: AuthProtocol.Type = Auth.self

И назначьте его следующим образом

sut.auth = SpyAuth.self
0 голосов
/ 02 февраля 2019

Звонок в 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, которую я сейчас пишу .)

...