XCUITest для начинающих: как сделать тестирование iOS красивым с Allure

от автора

Привет!

Меня зовут Антон, и я занимаюсь автоматизацией тестирования Web и мобильных приложений.

Если вы начинаете автоматизировать UI-тесты под iOS, то наверняка заметили, что информации по фреймворку XCUITest в сети не так много, особенно на русском языке.

Эта статья — краткое руководство по основам автоматизации на XCUITest. Здесь я постарался собрать ключевые моменты, которые помогут вам сделать первые шаги в тестировании iOS-приложений.

Первые тесты

Переходим по ссылке на Github проект, на котором будем учиться UI тесты:

Далее нажимаем на Code -> Копируем ссылку с HTTPS

Скрин клонирования проекта
Клонирование проекта

Клонирование проекта

В проекте находим папку с тестами и в ней откроем класс SampleXCUITests. В классе удаляем все лишнее

Скрин пустого проекта
Холст iOS тестирования

Холст iOS тестирования

Метод setUp() запускается перед каждым тестом, метод tearDown() работает после каждого теста.

Автоматизируем Главный экран приложения

Проверка видимости Alert после нажатия на него

func testAlertShouldAppearAfterButtonTap() {         let alertButton = app.buttons["Alert"]             XCTAssertTrue(alertButton.waitForExistence(timeout: 3),                          "Кнопка 'Alert' не найдена на экране")         alertButton.tap()          let alert = app.alerts.element.staticTexts["Alert"]             XCTAssertTrue(alert.waitForExistence(timeout: 3),                       "Alert не появился после нажатия кнопки")     }

Проверка отсутствия видимости Alert после нажатия на кнопку ‘Ок’

func testAlertShouldDisappearAfterTappingOK() {         let alertButton = app.buttons["Alert"]             XCTAssertTrue(alertButton.waitForExistence(timeout: 3),                          "Кнопка 'Alert' не найдена на экране")         alertButton.tap()          let alert = app.alerts.element.staticTexts["Alert"]             XCTAssertTrue(alert.waitForExistence(timeout: 3),                       "Alert не появился после нажатия кнопки")          let alertButtonOK = app.alerts.element.buttons["OK"]         alertButtonOK.tap()          XCTAssertFalse(alert.waitForExistence(timeout: 3),                   "Alert должен исчезнуть после нажатия OK")     }

Проверка видимости символов после ввода в ‘поле ввода’

func testTextInputShouldDisplayCorrectly() {         let textButton = app.buttons["Text"]         XCTAssertTrue(textButton.waitForExistence(timeout: 3),                       "Кнопка 'Text' не найдена на экране")         textButton.tap()          let textField = app.textFields["Enter a text"]         let displayedText = app.staticTexts["VK"]         XCTAssertTrue(textField.waitForExistence(timeout: 3),                       "Поле ввода должно отображаться после нажатия кнопки")         textField.tap()         textField.typeText("VK")         app.keyboards.buttons["Return"].tap()         if displayedText.isEnabled {             XCTContext.runActivity(named: "Проверка отображения введённого текста") { _ in                 XCTAssertTrue(displayedText.waitForExistence(timeout: 3),                               "Текст 'VK' должен появиться на экране")                 XCTAssertEqual(displayedText.label, "VK",                                "Отображаемый текст должен точно соответствовать введенному")             }         }     }

Проверка видимости символов при переключение экранов после ввода в ‘поле ввода’

func testCheckVisibleTextWhileSwitchingBetweenScreens() {         let textButton = app.buttons["Text"]         XCTAssertTrue(textButton.waitForExistence(timeout: 3),                       "Кнопка 'Text' не найдена на экране")         textButton.tap()          let textField = app.textFields["Enter a text"]         let displayedText = app.staticTexts["VK"]         XCTAssertTrue(textField.waitForExistence(timeout: 3),                       "Поле ввода должно отображаться после нажатия кнопки")         textField.tap()         textField.typeText("VK")         app.keyboards.buttons["Return"].tap()         if displayedText.isEnabled {             XCTContext.runActivity(named: "Проверка отображения введённого текста") { _ in                 XCTAssertTrue(displayedText.waitForExistence(timeout: 3),                               "Текст 'VK' должен появиться на экране")                 XCTAssertEqual(displayedText.label, "VK",                                "Отображаемый текст должен точно соответствовать введенному")             }         }         XCTContext.runActivity(named: "Проверка сохранения текста после перехода на WebView") { _ in                 app.tabBars.buttons["Web View"].tap()                 app.tabBars.buttons["UI Elements"].tap()                                  XCTAssertTrue(displayedText.waitForExistence(timeout: 3),                             "Текст должен сохраняться при возврате на экран")                 XCTAssertEqual(displayedText.label, "VK",                              "Текст не должен изменяться при переключении экранов")         }     }

Все результаты тестов можно посмотреть в вкладке Show the Test navigator

Скрин результата прогона тестов
Результаты прогона тестов

Результаты прогона тестов

Усовершенствуем тесты

Такие тесты уже неплохие, но есть что добавить!

Первое что добавим, это разделим на Page Object и Page Element экраны приложения

В приложении 2 активных экрана: UI Elements(Главный экран) и Web View

Нажимаем правой мышкой на папку SampleXCUITests -> Нажимаем New Group -> Называем директорию

BaseScreen является базовым классом для всех экранов в UI-тестах, который содержит общую логику взаимодействия с пользовательским интерфейсом.

  • @discardableResult — аннотация используется для подавления предупреждений компилятора в случаях, когда возвращаемое значение метода не используется в коде.

  • Зачем возвращать Self? — возврат Self позволяет использовать методы класса последовательно (паттерн Chain of Responsibility)

BaseScreen.swift
import Foundation import XCTest  class BaseScreen {     public let app = XCUIApplication()     private lazy var uiElementsTab = app.tabBars.buttons["UI Elements"]     private lazy var webViewTab = app.tabBars.buttons["Web View"]     private lazy var localTestingTab = app.tabBars.buttons["Local Testing"]          @discardableResult     func goToUIElements() -> Self {         uiElementsTab.tap()         return self     }          @discardableResult     func goToWebView() -> Self {         webViewTab.tap()         return self     }          @discardableResult     func goToLocalTesting() -> Self {         localTestingTab.tap()         return self     } }

Verifications расширил класс XCUIElement (представляет собой набор вспомогательных методов для работы с элементами пользовательского интерфейса в UI-тестах), чтобы не плодить много кода по проверкам после нажатия, проверку видимости элементов и т.д.

Verifications.swift
import Foundation import XCTest  extension XCUIElement {     @discardableResult     func verifyExistence(timeout: TimeInterval = 3, message: String = "") -> Self {         let errorMessage = message.isEmpty ? "Элемент '\(self)' должен существовать" : message         XCTAssertTrue(             self.waitForExistence(timeout: timeout),             errorMessage         )         return self     }          @discardableResult     func verifyHittable(message: String = "") -> Self {         let errorMessage = message.isEmpty ? "Элемент '\(self)' должен быть доступен для взаимодействия" : message         XCTAssertTrue(             self.isHittable,             errorMessage         )         return self     }          @discardableResult     func verifyDisappear(timeout: TimeInterval = 3, message: String = "") -> Self {         let errorMessage = message.isEmpty ? "Элемент '\(self)' должен исчезнуть" : message         XCTAssertFalse(             self.waitForExistence(timeout: timeout),             errorMessage         )         return self     }          @discardableResult     func verifyAndTap(timeout: TimeInterval = 3, message: String = "") -> Self {         self.verifyExistence(timeout: timeout, message: message)             .verifyHittable(message: message)             .tap()         return self     }          @discardableResult     func verifyLabel(expected: String, message: String = "") -> Self {         let errorMessage = message.isEmpty ?         "Текст элемента '\(self)' не соответствует. Актуальный: '\(self.label)', Ожидаемый: '\(expected)'" :         message         XCTAssertEqual(self.label, expected, errorMessage)         return self     }               @discardableResult     func typeTextSafely(_ text: String, message: String = "") -> Self {         self.verifyExistence(message: message)             .verifyHittable(message: message)             .tap()         self.typeText(text)         XCUIApplication().keyboards.buttons["Return"].tap()         return self     }          @discardableResult     func scrollToView(maxAttempts: Int = 10) -> Self {         for _ in 1...maxAttempts {             if self.isHittable {                 return self             }             sleep(2)             XCUIApplication().webViews.firstMatch.swipeUp()         }         XCTFail("Элемент \(self) отсутствует на экране")         return self     } } 

HomeScreen — класс для взаимодействия с элементами главного экрана.

Нижнее подчеркивание около внешнего имени параметра (_ inputMessage: String) нужно, чтобы при вызове метода не писать около имени параметра внешнее имя.

HomeScreen.swift
import Foundation import XCTest  final class HomeScreen: BaseScreen {     private lazy var titleLabel = app.navigationBars.staticTexts["UI Elements"]     private lazy var buttonText = app.buttons["Text"]     private lazy var alertButton = app.buttons["Alert"]     private lazy var alertText = app.alerts.element.staticTexts["Дуров верни стену"]     private lazy var alertOKButton = app.alerts.element.buttons["😔"]     private lazy var buttonBack = app.navigationBars.buttons["UI Elements"]     private lazy var textField = app.textFields["Enter a text"]     private lazy var resultLabel = app.staticTexts[HomeScreenValue.textFieldInput]          lazy var baseElement = titleLabel          @discardableResult     func tapText() -> Self {         buttonText             .verifyExistence()             .verifyHittable()             .tap()                  textField.verifyExistence()         return self     }          @discardableResult     func tapAlert() -> Self {         alertButton             .verifyExistence()             .verifyHittable()             .tap()                  alertText             .verifyExistence()         return self     }          @discardableResult     func closeAlert() -> Self {         alertOKButton             .verifyExistence()             .verifyHittable()             .tap()                  alertOKButton.verifyDisappear()         return self     }          @discardableResult     func enterText(_ inputMessage: String) -> Self {         textField.typeTextSafely(inputMessage)         return self     }          @discardableResult     func checkTextAfterPushTextField(_ expectedText: String) -> Self {         resultLabel.verifyLabel(expected: expectedText)         return self     } }

WebViewScreen — класс для взаимодействия с элементами экрана построенного на WebView.

WebViewScreen.swift
import Foundation import XCTest  final class WebViewScreen: BaseScreen {     private lazy var titleLabel = app.webViews.staticTexts["App & Browser Testing Made Easy"]     private lazy var benefitsSection = app.webViews.staticTexts["Benefits"]          lazy var baseElement = titleLabel          @discardableResult     func verifyBenefitsSectionVisible() -> Self{         benefitsSection.scrollToView()         return self     }          @discardableResult     func shouldFindTextByPrefix() -> Self{         let beginText = app.webViews.staticTexts.containing(NSPredicate(format: "label BEGINSWITH %@", "Give your")).firstMatch         beginText.verifyExistence()         return self     }          @discardableResult     func shouldFindTextCaseInsensitive() -> Self {         let containsText = app.webViews.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] %@", "give your")).firstMatch         containsText.verifyExistence()         return self     }          @discardableResult     func shouldFindTextByMultipleKeywords(_ keywords: [String]) -> Self{         let keyText = app.webViews.staticTexts.containing(NSPredicate(format:                        "label CONTAINS %@ AND label CONTAINS %@",         keywords[0], keywords[1])).firstMatch         keyText.verifyExistence()         return self     } }

Вынесем определенные строки в константы (для главного экрана и для ошибок в случае не загрузки экранов соответственно)

public enum HomeScreenValue {     public static let textFieldHint = "Waiting for text input."     public static let textFieldInput = "XCUI Tests" }
public enum ErrorMessageValue {     public static let loadMainScreen = "Главный экран не загрузился"     public static let loadWebViewScreen = "Экран WebView не загрузился" }

BaseTests — базовый класс для UI-тестов, который инициализирует приложение, автоматически запускает его перед каждым тестом и завершает после выполнения

class BaseTests: XCTestCase {     private var baseScreen = BaseScreen()      lazy var app = baseScreen.app      open override func setUp() {         app.launch()         // Тест остановится при первой ошибке         continueAfterFailure = true     }      open override func tearDown() {         app.terminate()     } }

continueAfterFailure — это свойство, которое определяет поведение теста при возникновении ошибки. По умолчанию оно установлено в true, что означает продолжение выполнения теста даже после обнаружения ошибки

Пример:

continueAfterFailure = false

Пример работы continueAfterFailure с флагом false

Пример работы continueAfterFailure с флагом false

continueAfterFailure = true

Пример работы continueAfterFailure с флагом true

Пример работы continueAfterFailure с флагом true

HomeScreenTests — класс для UI-тестов главного экрана. Написал проверку загрузки экрана в setUp() (по типу паттерна LoadableComponent)

HomeScreenTests.swift
import Foundation import XCTest  final class HomeScreenTests: BaseTests {          override func setUp() {         super.setUp()         HomeScreen().baseElement.verifyExistence(             message: ErrorMessageValue.loadMainScreen         )     }          func testAlertShouldAppearAfterButtonTap() {         HomeScreen()             .tapAlert()     }          func testAlertShouldDisappearAfterTappingOK() {         HomeScreen()             .tapAlert()             .closeAlert()     }          func testTextInputShouldDisplayCorrectly() {         HomeScreen()             .tapText()             .enterText(HomeScreenValue.textFieldInput)             .checkTextAfterPushTextField(HomeScreenValue.textFieldInput)     }          func testCheckVisibleTextWhileSwitchingBetweenScreens() {         HomeScreen()             .tapText()             .enterText(HomeScreenValue.textFieldInput)         BaseScreen()             .goToWebView()             .goToUIElements()         HomeScreen()             .checkTextAfterPushTextField(HomeScreenValue.textFieldInput)     } }

WebViewScreenTests — класс для UI-тестов WebView экрана

WebViewScreenTests.swift
import Foundation  final class WebViewScreenTests: BaseTests {          override func setUp() {         super.setUp()         BaseScreen()             .goToWebView()         WebViewScreen()             .baseElement             .verifyExistence(message: ErrorMessageValue.loadWebViewScreen)     }          func testScrollToTextBenefits() {         WebViewScreen()             .verifyBenefitsSectionVisible()     }          // Поиск по началу текста     func testShouldFindTextByPrefix() {         WebViewScreen()             .shouldFindTextByPrefix()     }          // Поиск с игнорированием регистра     func testShouldFindTextCaseInsensitive() {         WebViewScreen()             .shouldFindTextCaseInsensitive()     }          // Поиск по ключевым словам     func testShouldFindTextByMultipleKeywords() {         WebViewScreen()             .shouldFindTextByMultipleKeywords(["users", "seamless"])     } }

Обновлённая структура проекта

Скрин структуры проекта
Архитектура проекта

Архитектура проекта

[Лайфхак] Чтобы выровнять код, то выделяете весь код (Command + A) -> Editor -> Re-Indent

Скрин выравнивания кода
Выравнивание кода в Xcode

Выравнивание кода в Xcode

Добавление Allure

Как внедрить Allure в проект — рассказали в статье

Расширил XCTest для поддержки Allure-отчетов, позволяя структурировать тесты по методологиям BDD (Epic, Feature, Story) и добавлять метаданные

Функции и их аннотации:

  • epic(_ values:String…) — Высокоуровневая бизнес-категория тестов

  • feature(_ values:String…) — Функциональный модуль

  • story(_ values: String…) — Юзер-стори или сценарий

  • displayName(_ name: String) — Имя теста в отчете

  • severity(_ values: String…) — Критичность теста

  • owner(_ values: String…)— Ответственный за тест

  • step(_ name: String, step: () -> Void) — Шаг теста

AllureXCTestExtensions.swift
import Foundation import XCTest  extension XCTest {     func epic(_ values:String...) {         label(name: "epic", values: values)     }          func feature(_ values:String...) {         label(name: "feature", values: values)     }          func story(_ values: String...) {         label(name: "story", values: values)     }          func displayName(_ name: String) {         addTestCaseName(value: name)     }          func severity(_ values: String...) {         label(name: "severity", values: values)     }          func owner(_ values: String...) {         label(name: "owner", values: values)     }          func step(_ name: String, step: () -> Void) {         XCTContext.runActivity(named: name) { _ in             step()         }     }          private func label(name: String, values: [String]) {         for value in values {             XCTContext.runActivity(named: "allure.label.\(name):\(value)", block: { _ in })         }     }          private func addTestCaseName(value: String) {         XCTContext.runActivity(named: "allure.name:\(value)") { _ in }     } }

Провели рефакторинг тестовых классов, добавив Allure-аннотации для улучшения структуры и прозрачности тестирования.

Модернизировали класс тестирования Главного экрана

HomeScreenTests.swift
import Foundation import XCTest   final class HomeScreenTests: BaseTests {          override func setUp() {         super.setUp()         step("Проверяем корректную загрузку главного экрана приложения") {             HomeScreen().baseElement.verifyExistence(                 message: ErrorMessageValue.loadMainScreen             )         }     }          // MARK: - Alert     func testAlertShouldAppearAfterButtonTap() {         epic("Главный экран")         feature("Взаимодействие с Alert")         story("Открытие Alert")         displayName("Открытие Alert при нажатии на кнопку")         severity("MINOR")         owner("Anton Moskovsky")                  step("Тап на кнопку Alert и проверка появления алерта") {             HomeScreen()                 .tapAlert()         }              }          func testAlertShouldDisappearAfterTappingOK() {         epic("Главный экран")         feature("Взаимодействие с Alert")         story("Закрытие Alert")         displayName("Закрытие Alert после нажатия на кнопку 'OK'")         severity("MINOR")         owner("Anton Moskovsky")                  step("Тап на кнопку Alert и проверка появления алерта") {             HomeScreen().tapAlert()         }                  step("Нажимаем кнопку 'OK' в Alert и проверяем его исчезновение") {             HomeScreen().closeAlert()         }     }          // MARK: - Ввод символов     func testTextInputShouldDisplayCorrectly() {         epic("Главный экран")         feature("Взаимодействие с текстовым полем")         story("Ввод и проверка текста")         displayName("Корректное отображение введенного текста")         severity("MINOR")         owner("Anton Moskovsky")                  step("Переходим на экран ввода текста через кнопку 'Text'") {             HomeScreen().tapText()         }                  step("Вводим текст '\(HomeScreenValue.textFieldInput)' в текстовое поле") {             HomeScreen()                 .enterText(HomeScreenValue.textFieldInput)         }                  step("Проверяем, что введенный текст корректно отображается над полем ввода") {             HomeScreen().checkTextAfterPushTextField(HomeScreenValue.textFieldInput)         }     }          func testCheckVisibleTextWhileSwitchingBetweenScreens() {         epic("Главный экран")         feature("Взаимодействие с текстовым полем")         story("Сохранение текста между экранами")         displayName("Сохранение текста при переключении экранов")         severity("NORMAL")         owner("Anton Moskovsky")                  step("Переходим на экран ввода текста через кнопку 'Text'") {             HomeScreen().tapText()         }                  step("Вводим текст '\(HomeScreenValue.textFieldInput)' в текстовое поле") {             HomeScreen()                 .enterText(HomeScreenValue.textFieldInput)         }                  step("Переход на WebView и обратно на главный экран") {             BaseScreen()                 .goToWebView()                 .goToUIElements()         }                  step("Проверяем, что введенный текст сохранился после возврата на главный экран") {             HomeScreen()                 .checkTextAfterPushTextField(HomeScreenValue.textFieldInput)         }     } }

И также обновили класс тестирования экрана на WebView

WebViewScreenTests.swift
import Foundation  final class WebViewScreenTests: BaseTests {          override func setUp() {         super.setUp()         step("Переходим на экран WebView из главного меню") {             BaseScreen()                 .goToWebView()         }         step("Проверяем корректную загрузку экрана WebView") {             WebViewScreen()                 .baseElement                 .verifyExistence(message: ErrorMessageValue.loadWebViewScreen)         }     }          func testScrollToTextBenefits() {         epic("Экран WebView")         feature("Поиск текста")         story("Нахождение текста с помощью ScrollView")         displayName("Поиск текста с помощью скролл")         severity("MINOR")         owner("Anton Moskovsky")         step("Выполняем скролл до раздела Benefits") {             WebViewScreen()                 .verifyBenefitsSectionVisible()         }     }          // Поиск по началу текста     func testShouldFindTextByPrefix() {         epic("Экран WebView")         feature("Поиск текста")         story("Поиск по началу текста")         displayName("Поиск текста по префиксу")         severity("MINOR")         owner("Anton Moskovsky")                  step("Ищем текст по начальным символам") {             WebViewScreen()                 .shouldFindTextByPrefix()         }     }          // Поиск с игнорированием регистра     func testShouldFindTextCaseInsensitive() {         epic("Экран WebView")         feature("Поиск текста")         story("Поиск с игнорированием регистра")         displayName("Поиск текста без учета регистра")         severity("MINOR")         owner("Anton Moskovsky")                  step("Ищем текст, игнорируя регистр символов") {             WebViewScreen()                 .shouldFindTextCaseInsensitive()         }     }          // Поиск по ключевым словам     func testShouldFindTextByMultipleKeywords() {         epic("Экран WebView")         feature("Поиск текста")         story("Поиск по ключевым словам")         displayName("Поиск текста по нескольким ключевым словам")         severity("MINOR")         owner("Anton Moskovsky")                  step("Ищем текст по ключевым словам") {             WebViewScreen()                 .shouldFindTextByMultipleKeywords(["users", "seamless"])         }     } }

Выполняем команду в cmd директории проекта:

allure serve allure-results

Результат отчета после прогона тестов

Отчет Allure
Результат отчета

Результат отчета

Полезные ресурсы


ссылка на оригинал статьи https://habr.com/ru/articles/909558/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *