Привет!
Меня зовут Антон, и я занимаюсь автоматизацией тестирования Web и мобильных приложений.
Если вы начинаете автоматизировать UI-тесты под iOS, то наверняка заметили, что информации по фреймворку XCUITest в сети не так много, особенно на русском языке.
Эта статья — краткое руководство по основам автоматизации на XCUITest. Здесь я постарался собрать ключевые моменты, которые помогут вам сделать первые шаги в тестировании iOS-приложений.
Первые тесты
Переходим по ссылке на Github проект, на котором будем учиться UI тесты:
Далее нажимаем на Code -> Копируем ссылку с HTTPS
Скрин клонирования проекта
В проекте находим папку с тестами и в ней откроем класс SampleXCUITests. В классе удаляем все лишнее
Скрин пустого проекта
Метод 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 = 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
Скрин выравнивания кода
Добавление 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://github.com/moskkovsky/ui-tests-xcui
-
Русскоязычный гайд по XCUITest: https://testengineer.ru/bolshoj-gajd-po-avtomatizacii-xcuitest/
-
Внедрение Allure (отчётность) в UI-тесты (swift, XCTest): https://habr.com/ru/companies/rtlabs/articles/686448/
-
Еще про Allure с iOS: https://kolesa.group/media/posts/tech-papers/kak-dzhun-vnedryal-allure-s-xctest-opyt-avtomatizacii-testirovaniya-ios
-
Про аннотации Allure: https://habr.com/ru/companies/sberbank/articles/359302/
ссылка на оригинал статьи https://habr.com/ru/articles/909558/
Добавить комментарий