Большой гайд по автоматизации в XCUITest

В 2021 году iOS-приложения должны строиться с расчетом на современный дизайн, обрабатывать множество одновременных запросов, работать с REST API и большими объемами статических данных. Это выдвигает высокие требования к качеству кода.

Чтобы достичь его, кажется не обойтись без автоматизированных тестов UI-интерфейса. Таким образом быстро проверяется поведение приложения при любых действиях пользователя. Фреймворк XCTest от Apple дает возможность писать UI-тесты с довольно широкой функциональностью.

Содержание

Добавление UI-тестов в имеющийся проект Xcode

При добавлении UI-тестов в уже созданный проект в Xcode они вставляются в отдельную группу и получают отдельный build target; так происходит потому что UI-тесты компилируются и деплоятся в отдельном приложении — тест-раннере XCUIApplication. Код созданных тестов запускается в этом раннере, а НЕ в целевом (окончательном, target) приложении. Раннер работает как прокси, принимает логику теста, и преобразует ее в некие действия (iOS Accessibility Actions), и уже эти действия выполняются в target-приложении. Таким образом имитируется взаимодействие пользователя с приложением. Это значит, с точки зрения программиста, что тестировщик не работает непосредственно с UKit-элементами приложения (типа UILabel или UIButton), а только через прокси-элементы (XCUIElement).

Добавление в новый проект 

При создании нового проекта XCode отмечаем опцию “Include Tests” (Включить тесты в проект).

xcui test xcode

После успешного создания нового проекта увидим в Навигаторе проектов (Project Navigator):

  • Группу UITests, уже с созданными и включенными в проект UI-тестами
  • И правее — билд нового проекта, с указанным target-приложением.
xcode добавление xcuitest

Добавление в существующий проект

Если проект уже есть и в него надо просто добавить тесты, нажимаем в главном меню XCode: File > New > Target… . Появится диалог выбора шаблона, в нем надо пролистать до раздела Test и выбрать там “UI Testing Bundle”.

xcuitest добавление в существующий проект

После нажатия “Next”, вводим название набора тестов (группы), или XCode сгенерит нам имена по умолчанию.

Класс UI Test

Это Swift-класс, наследуемый из XCTestCase. Пройдемся по структуре этого класса.

Настройка 

Класс теста может содержать лишь один метод с настройкой. Объявление метода выглядит так:

override func setUpWithError() throws

Перед запуском каждого теста XCTest вызывает метод setUpWithError(), чтобы можно было настроить тест. Мы оверрайдим метод, поскольку он наследуется от суперкласса XCTestCase.

Если непонятно, что throws делает в данном контексте — оно позволяет методу передавать ошибки, произошедшие во время выполнения, в коллер фреймворка XCTest.

XCUIApplication

Теперь вспоминаем, что написанный тест вызывается и запускается из раннера, а не из основного приложения. XCUIApplication, еще раз, является прокси-объектом, как бы отображающим основное приложение. Обычно объявляется переменная XCUIApplication в тестовом классе, и вызывается метод:

class ExampleUITests: XCTestCase {
    var app: XCUIApplication!

    override func setUpWithError() throws {
        try super.setUpWithError()
        
        app = XCUIApplication()
        app.launch()
    }
}

Метод запуска является синхронным, он возвращает прокси-объект, отображающий основное приложение.

Или же, можно задать настройки XCUIApplication чтобы оно приняло как target предустановленное приложение на реальном iOS-устройстве. Для этого пишем: Bundle Identifier. Например чтобы запустить Safari на смартфоне:

app = XCUIApplication(bundleIdentifier: "com.apple.mobilesafari")

XCUIDevice

Фреймворк позволяет управлять ориентацией экрана, эмулировать нажатие кнопок, и даже управление через Siri, посредством прокси-объекта XCUIDevice. Он представляет собой синглтон, к которому получают доступ через его свойство   .shared :

let device = XCUIDevice.shared

В этом классе часто вызывается изменение ориентации экрана. За это отвечает свойство opientation. Для задания ориентации вводится enum-значение UIDeviceOrientation. Могут быть такие ориентации:

  • UIDeviceOrientation.portrait
  • UIDeviceOrientation.landscapeLeft
  • UIDeviceOrientation.landscapeRight
  • UIDeviceOrientation.portraitUpsideDown

К примеру, задаем портретную ориентацию в методе, чтобы смартфон сохранял портретную ориентацию при выполнении UI-тестов:

class ExampleUITests: XCTestCase {
    var app: XCUIApplication!
    let device = XCUIDevice.shared

    override func setUpWithError() throws {
        try super.setUpWithError()
        
        app = XCUIApplication(bundleIdentifier: "com.apple.mobilesafari")
        app.launch()
        
        device.orientation = UIDeviceOrientation.portrait
    }

Teardown

Здесь так же: тестовый класс может содержать в себе лишь один teardown-метод. Объявляется так:

override func tearDownWithError() throws

В методе выполняются любые cleanup-задачи, или выбрасываются ошибки после завершения теста. Оверрайдим этот метод, потому что он наследуется из суперкласса XCTestCase. Обычная teardown-задача заключается в закрытии приложения во время тестирования:

override func tearDownWithError() throws {
        try super.tearDownWithError()

        app.terminate()
}

Методы в тесте

В нашем классе можно создать новые тест-кейсы, путем создания новых методов, и каждый из них это отдельный UI-тест. Принято присваивать названия методам, добавляя в их начало test:

func testTapOnRefreshButton() throws {
	// Test logic here...
} 

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

Взаимодействие с UI-элементами

Мы уже применили концепцию XCUIApplication как прокси-объекта, отображающего основное приложение. Класс XCUIApplication наследуется из класса XCUIElement, в рамках протокола XCUIElementTypeQueryProvider. Важность этого, с точки зрения разработчика, заключается в том, что имея экземпляр XCUIApplication, мы можем отправлять ему запросы, и получать доступ к видимым элементам любого View (экрана) в приложении.

Сначала пройдемся по свойствах XCUIElementTypeQueryProvider по части идентификации элементов. Есть часто применяемые методы и свойства.

Получение элементов экрана

XCUIElementTypeQueryProvider это протокол, который синхронизируется с XCUIApplication, передающем элементы для идентификации. Каждое свойство возвращает объект XCUIElementQuery, по которому находим элемент. Ниже приведены некоторые свойства:

  • var alerts: XCUIElementQuery

Доступ к любому предупреждению, если оно видимое на экране.

  • var buttons: XCUIElementQuery

Доступ ко всем видимым кнопкам на экране (UIButton)

  • var staticTexts: XCUIElementQuery

Доступ ко всем видимым текстовым надписям (UILabel) на экране.

  • var textFields: XCUIElementQuery

Доступ ко всем полям ввода текста (UITextField)

  • var tables: XCUIElementQuery

Доступ ко всем таблицам (UITableView)

  • var otherElements: XCUIElementQuery

Последнее название может озадачить, а это свойство очень полезное. Оно возвращает строку, дающую доступ к другим под-экранам (UIView) в открытом экране. Иногда дизайнеры применяют UIView для создания кастомных кнопок. Такое часто делают в iOS-приложениях, написанных на React Native.

Надо учесть, что эти свойства  возвращают XCUIElementQuery для всех элементов указанного типа. Можно уточнить запрос, чтобы идентифицировать отдельный элемент:

let companyNameTextElement = app.staticTexts["Tesla, Inc."]

Если строка “Tesla, Inc.” есть на экране, то companyNameTextElement будет содержать результат XCUIElement. Чуть позже рассмотрим свойства и методы XCUIElement. Однако, такой подход не очень-то гибкий. А что если название компании в другом формате? Есть два подхода для выполнения такого запроса: через строку-предикат; или через идентификатор доступности. Хотя это разные способы, оба они позволяют корректно конструировать XCUIElementQuery. Теперь выясним некоторые свойства и методы XCUIElementQuery.

  • var count: Int

Обрабатывает запрос и возвращает количество элементов, отвечающих условию.

  • var element: XCUIElement

Возвращает первый подходящий элемент в запросе.

  • func element(matching: XCUIElement.ElementType, identifier: String?) -> XCUIElement

Возвращает элемент, соответствующий указанному типу, и строке идентификатора доступности.

Строка-предикат 

Так называется некое логическое условие, созданное по соответствующей спецификации Apple. Это достаточно специфическая тема, если интересно, можно ознакомиться с предметом по ссылке. Ниже — пример, как строка, построенная по “предикативному принципу” корректирует запрос:

let predicate = NSPredicate(format: "label CONTAINS[c] 'tesla'")
let textQuery = app.staticTexts.containing(predicate)

// Check to see if the query contains results
if textQuery.count > 0 {
  let companyNameTextElement = textQuery.element
}

Строка-предикат делает запрос гибче: проводится проверка (без учета регистра) в тексте на наличие слова “tesla”.

Примечание: независимость от регистра — это символ [c]

Вместе с тем, метод строки-предиката должно нечасто применяться, по идее. Хотя она и дает больше гибкости, но также и больше сложности в логическом плане. Делает код автоматизации сложнее в чтении. Лучше все-таки в основном применять идентификаторы доступности для обращения к элементам.

Идентификатор доступности

Это строка, прибавляемая к UIKit-элементу, уникально идентифицирующая его в тесте. Нечто вроде как прикреплять ярлычок на странице книжки, чтобы потом быстрее открыть в нужном месте.

Сториборды 

Если в приложении есть сториборды, то можно визуально выделить элемент интерфейса в сториборде, и ввести в Инспекторе (Identity Inspector) строку, содержащую Идентификатор доступности:

сториборды xcuitest

Swift UI

Если дизайн приложения делается при помощи SwiftUI, можно добавить в код соответствующий идентификатор, например:

Button(action: {}, label: { Text("Submit") }).accessibility(identifier: "stockTickerSymbolSubmitBtn")

React Native

Если приложение пишется на React Native, то тоже можно добавлять в код такие идентификаторы. 

Далее пример, как можно найти элемент по его идентификатору:

let companyNameTextElement = app.staticTexts.element(matching: XCUIElement.ElementType.staticText, identifier: «stockNameText»)

В методе элемента мы вводим 2 параметра:

matching: Тип элемента XCUIElement

identifier: Строка с идентификатором доступности

Свойства элементов

Выше мы исследовали способы запроса элементов XCUIElement. После получения элемента, видим что есть некоторые его свойства, с которыми можно поработать:

  • var exists: Bool

Существует ли такой элемент

  • var isHittable: Bool

Можно ли нажать этот элемент в этом его положении

  • var label: String

Атрибут подписи элемента

  • var placeholderValue: String?

Значение, отображаемое в случае, если значение не задавалось

  • var value: Any?

Raw-value-атрибут элемента

  • var isEnabled: Bool

Можно ли взаимодействовать с элементом нажатием на него

Например. Если хотим передать строку из XCUIElement со значением UILabel на одном из экранов приложения:

let companyNameText = app.staticTexts.element(matching: XCUIElement.ElementType.staticText, identifier: "stockNameText")

print("Company name text: \(companyNameText.label)")

Действия с элементами

Предусмотрено много действий с XCUIElement. 

  • func tap()

имитирует нажатие элемента

  • func doubleTap()

имитирует двойное нажатие

  • func press(forDuration: TimeInterval)

имитирует длительное нажатие (с указанием длительности)

  • func typeText(String)

Вводит строку в элементе

  • func swipeLeft()

Имитирует свайп влево

  • func swipeRight()

Свайп вправо

  • func swipeUp()

Свайп вверх

  • func swipeDown()
  • Свайп вниз

Пробуем: чтобы передать текст в некий XCUIElement содержащий поле UITextField и имитировать нажатие другого XCUI-элемента с UIView работающего как кастомная кнопка:

let textInputField = app.textFields.element(matching: .textField, identifier: "stockTickerSymbolSearchInput")

let submitBtn = app.otherElements.element(matching: .other, identifier: "stockTickerSymbolSearchBtn")

if (textInputField.exists && submitBtn.exists) {
    textInputField.tap()
    textInputField.typeText("TWTR")
    submitBtn.tap()
}

Assertions

Концепция Assertions (Утверждения) позволяют сравнивать результат с ожидаемым значением. Применяются для проверки выполнения действий в автоматизированном тесте — отвечают ли ожидаемому поведению. В Apple предусмотрели специальный набор макросов XCTAssert, некоторые приведены ниже:

  • XCTAssert(expression)

Простая проверка, правильно ли выражение. Возвращает сбой, если выражение == “false”.

  • XCTAssertEqual(expression1, expression2)

Проверка на совпадение значений двух выражений. Сбой, если значения совпали. (expression1 != expression2).

  • XCTAssertNotEqual(expression1, expression2)

“Утверждение”, что два выражения не имеют одинаковое значение. Сбой, если expression1 == expression2.

  • XCTAssertNil(expression)

Проверка на равность 0. Сбой, если выражение != 0.

Пример:

func testExample() throws {
	let textInputField = app.textFields.element(matching: .textField, identifier: "stockTickerSymbolSearchInput")
	let submitBtn = app.otherElements.element(matching: .other, identifier: "stockTickerSymbolSearchBtn")
	if (textInputField.exists && submitBtn.exists) {
		textInputField.tap()
    textInputField.typeText("TWTR")
    submitBtn.tap()
  }
        
  let companyNameText = app.staticTexts.element(matching: .staticText, identifier: "stockNameText")
  XCTAssertEqual(companyNameText.label, "Twitter, Inc.")
}

Запуск тестов

Из Навигатора тестов в XCode

Простой интуитивный запуск из IDE. В Навигаторе Тестов справа будут ромбовидные кнопки для каждого тестового класса, добавленного в группу. Доступны опции запуска:

  • Всех тестов в группе (Test Group)
  • Запуска одного тест-сьюта (Test Suite)
  • Запуска отдельно функции (Test Case function) в тест-сьюте

Также можно запустить тестирование непосредственно в коде.

xcode запуск тестов xcui test

Новая Схема тестов в XCode

Обычно при тестировании создается новая билд-схема для запуска автоматических UI-тестов. Главная причина в том, что если запускать тесты из меню в XCode: Product > Test (или клавиатурной комбинацией), то например юнит-тесты (если таковые есть в проекте) будут запускаться первыми, перед UI-тестами. То есть, надо будет ждать завершения юнит-тестов, чтобы запустились UI-тесты.

Чтобы избежать этого, создаем билд-схему “только UI-тесты”. В главном меню XCode выбираем Product > Scheme > Manage Schemes… . Теперь увидим список имеющихся билд-схем проекта.

схема тестов xcuitest

Кликаем на текущей схеме, переходим к значку настроек в левом нижнем углу списка схем, и выбираем Duplicate.

xcuitest схема тестов

Вводим название новой схемы, кликаем на разделе Test, и снимаем галочку с первой группы, где и будут юнит-тесты.

xcuitest ввод названия схемы тестов

Теперь можно запустить схему из меню Product > Test в главном меню XCode (или шоткатом).

Применение CLI-интерфейса xcodebuild

При установке XCode и его сопутствующих CLI-тулзов, ставится и xcodebuild, путь к которому прописывается в системных PATH-переменных. Можно запускать UI-тесты с xcodebuild прямо из командной строки, не открывая XCode. 

$ xcodebuild \
  -project "Example.xcodeproj" \
  -scheme "Stock Price App UI Tests" \
  -destination "platform=iOS Simulator,name=iPhone 12 Pro Max,OS=14.2" \
  test
  • project

Это путь к проекту, .xcodeproj

  • -scheme

Название схемы. 

  • -destination

Путь к устройству, выражаемый в виде строки. Чтобы уточнить, какие девайсы зарегистрированы в системе, набираем:

$ xcrun xctrace list devices

Тестирование черного ящика

Как мы уже знаем, можно запускать XCUIApplication чтобы протестировать свое приложение на физическом устройстве подключенном к компьютеру, через идентификатор бандла, то есть мы можем писать тесты XCUITests для приложений, кода которых у нас нет. Однако, нам все же надо посмотреть иерархию вьюшек в приложении. Для этого можем воспользоваться такой вещью как Appium Desktop. 

xcuitest тестирование черного ящика

Можно инспектировать элементы каждого View, чтобы видеть их XCUIElement.ElementType и их атрибуты и идентификаторы если они доступны — надписи, строки и т.п.

Теперь мы знаем как тестируется интерфейс в XCode-приложениях, и умеем писать простые UI-тесты.

Какой была ваша первая зарплата в QA и как вы искали первую работу?

Мега обсуждение в нашем телеграм-канале о поиске первой работы. Обмен опытом и мнения.

1 КОММЕНТАРИЙ

Подписаться
Уведомить о
guest

1 Комментарий
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
Serg
Serg
2 лет назад

Спасибо, как раз готовлю доклад по возможностям XCTest, как раз кстати!

Мы в Telegram

Наш официальный канал
Полезные материалы и тесты
Готовимся к собеседованию
Project- и Product-менеджмент

? Популярное

? Telegram-обсуждения

Наши подписчики обсуждают, как искали первую работу в QA. Некоторые ищут ее прямо сейчас.
Наши подписчики рассказывают о том, как не бояться задавать тупые вопросы и чувствовать себя уверенно в новой команде.
Обсуждаем, куда лучше податься - в менеджмент или по технической ветке?
Говорим о конфликтных ситуациях в команде и о том, как их избежать
$1100*
медианная зарплата в QA в июне 2023

*по результатам опроса QA-инженеров в нашем телеграм-канале

Собеседование

19%*
IT-специалистов переехало или приняло решение о переезде из России по состоянию на конец марта 2022

*по результатам опроса в нашем телеграм-канале

live

Обсуждают сейчас