July 26, 2024

77. Пишем UI-тесты для SwiftUI-экрана

В предыдущей статье я показал как можно написать unit-тесты для модели, которая является источником истины в SwiftUI-экране. В этой статье покажу как можно написать ui-тесты для того же экрана.

Разработчик вручную проводит UI-тест

Для чего нужны ui-тесты

На моем опыте чаще всего ui-тесты используются в качестве E2E-тестов.

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

Основные аспекты E2E тестов

  • Полное покрытие сценариев: E2E тесты охватывают все ключевые сценарии использования приложения, включая взаимодействие с пользовательским интерфейсом, сетевыми запросами и базами данных. Это позволяет убедиться, что приложение работает как единое целое.
  • Проверка бизнес-логики: эти тесты помогают проверить, что бизнес-логика приложения выполняется правильно, и все функции работают так, как задумано.
  • Обнаружение регрессионных ошибок: E2E тесты могут выявлять проблемы, которые могут возникнуть после внесения изменений в код, что особенно важно в больших проектах с множеством зависимостей.
  • Автоматизация тестирования: E2E тесты могут быть автоматизированы, что позволяет запускать их на каждом этапе разработки, например, при каждом Pull Request, что значительно ускоряет процесс тестирования и повышает его эффективность.
  • Улучшение пользовательского опыта: регулярное тестирование помогает выявлять и устранять проблемы, которые могут негативно сказаться на пользовательском опыте, что в свою очередь может повысить удовлетворенность пользователей.

Пишем тесты

Подготовка проекта

Таргет для ui-тестов у нас уже был (создается по умолчанию), осталось добавить файл для тестов:

Слева - структура файлов, выделены ui-тесты, а справа - таргеты проекта

Подготовка файла для тестов

Для тестов нам потребуются:

  • import XCTest
  • класс-наследник XCTestCase
  • свойство для обращения к тестовой версии приложения XCUIApplication
import XCTest

final class LoginViewUITests: XCTestCase {
  private var app: XCUIApplication!
   
  override func setUpWithError() throws {
    // Инициализация приложения перед каждым тестом
    app = XCUIApplication()
    app.launch()
  }
   
  override func tearDownWithError() throws {
    app = nil
  }
}

Метод setUpWithError вызывается перед каждым тестом и инициализирует экземпляр XCUIApplication, а метод tearDownWithError очищает экземпляр после выполнения тестов.

Планируем сценарии для тестов

На мой взгляд нам хватит 4 метода для проверки основных сценариев на нашем экране (слева сценарий на русском, справа название тестового метода), и как обычно названия тестовых методов должны начинаться со слова test:

  1. Валидный логин, короткий пароль, кнопка авторизации должна быть недоступна - testLoginButtonDisabledWhenPasswordIsTooShort
  2. Короткий логин, валидный пароль, кнопка авторизации должна быть недоступна — testLoginButtonDisabledWhenUsernameIsTooShort
  3. Логин и пароль невалидные, кнопка авторизации должна быть недоступна - testLoginButtonDisabledWhenBothUsernameAndPasswordAreInvalid
  4. Логин и пароль валидные, кнопка авторизации должна быть доступна — testLoginButtonEnabledWhenCredentialsAreValid

Пишем тесты

Готовый файл с тестами:

import XCTest

final class LoginViewUITests: XCTestCase {
  private var app: XCUIApplication!
   
  override func setUpWithError() throws {
    // Инициализация приложения перед каждым тестом
    app = XCUIApplication()
    app.launch()
  }
   
  override func tearDownWithError() throws {
    app = nil
  }
   
  func testLoginButtonDisabledWhenUsernameIsTooShort() {
    // Вводим короткий логин
    let usernameTextField = app.textFields["Логин"]
    usernameTextField.tap()
    usernameTextField.typeText("abc")
     
    // Вводим валидный пароль
    let passwordTextField = app.secureTextFields["Пароль"]
    passwordTextField.tap()
    passwordTextField.typeText("ValidPassword1!")
     
    // Проверяем, что кнопка авторизации отключена
    let loginButton = app.buttons["Авторизоваться"]
    XCTAssertFalse(loginButton.isEnabled)
  }
   
  func testLoginButtonDisabledWhenPasswordIsTooShort() {
    // Вводим валидный логин
    let usernameTextField = app.textFields["Логин"]
    usernameTextField.tap()
    usernameTextField.typeText("validUser")
     
    // Вводим короткий пароль
    let passwordTextField = app.secureTextFields["Пароль"]
    passwordTextField.tap()
    passwordTextField.typeText("short")
     
    // Проверяем, что кнопка авторизации отключена
    let loginButton = app.buttons["Авторизоваться"]
    XCTAssertFalse(loginButton.isEnabled)
  }
   
  func testLoginButtonEnabledWhenCredentialsAreValid() {
    // Вводим валидный логин
    let usernameTextField = app.textFields["Логин"]
    usernameTextField.tap()
    usernameTextField.typeText("validUser")
     
    // Вводим валидный пароль
    let passwordTextField = app.secureTextFields["Пароль"]
    passwordTextField.tap()
    passwordTextField.typeText("ValidPassword1!")
     
    // Проверяем, что кнопка авторизации активна
    let loginButton = app.buttons["Авторизоваться"]
    XCTAssertTrue(loginButton.isEnabled)
  }
   
  func testLoginButtonDisabledWhenBothUsernameAndPasswordAreInvalid() {
    // Вводим короткий логин
    let usernameTextField = app.textFields["Логин"]
    usernameTextField.tap()
    usernameTextField.typeText("abc")
     
    // Вводим короткий пароль
    let passwordTextField = app.secureTextFields["Пароль"]
    passwordTextField.tap()
    passwordTextField.typeText("short")
     
    // Проверяем, что кнопка авторизации отключена
    let loginButton = app.buttons["Авторизоваться"]
    XCTAssertFalse(loginButton.isEnabled)
  }
}

Запускаем тесты

Короткий логин, валидный пароль, кнопка авторизации должна быть недоступна
Валидный логин, короткий пароль, кнопка авторизации должна быть недоступна
Логин и пароль невалидные, кнопка авторизации должна быть недоступна
Логин и пароль валидные, кнопка авторизации должна быть доступна

Заключение

Подходы к тестированию (и инструменты) могут отличаться в разных компаниях, но это некритично - важно, что теперь ты имеешь представление о unit/ui-тестах в iOS-разработке. Поздравляю!

Как видно, тут нет ничего супер-сложного. Более того, эти тесты можно сгенерировать при помощи разных инструментов (некоторые их по ошибке называют ИИ) - это означает, что аргумент "у нас нет времени на тесты" уже не принимается 💁‍♂️

Код для этой статьи можно посмотреть тут, другие статьи по разработке - тут, а про инвестиции - тут.