Данная статья является переводом статьи UI Testing in Swift with XCTest framework.
В этой статье мы не говорить о важности тестирования в целом, но я хочу поговорить о тестировании пользовательского интерфейса. Одним из очевидных преимуществ тестирования пользовательского интерфейса по сравнению с модульным тестированием является возможность писать тесты пользовательского интерфейса, даже если у вас есть пахнущая и глубоко связанная кодовая база. В этой статье мы рассмотрим, как писать тесты пользовательского интерфейса как для проектов на основе SwiftUI, так и для UIKit.
Основы
Тест пользовательского интерфейса - это программный способ проверить, правильно ли работает конкретный пользовательский поток в вашем приложении. Например, вы можете написать тест пользовательского интерфейса, имитирующий поведение пользователя при добавлении товара в корзину покупок и оформлении заказа.
Xcode предоставляет нам фреймворк XCTest, который мы используем для написания как unit, так и UI тестов. XCTest использует API специальных возможностей ( Accessibility API ) для доступа к элементам управления в вашей иерархии представлений и взаимодействия с ними. Как вы, наверное, знаете, UIKit и Swift UI предоставляют поддержку специальных возможностей "из коробки". Вот почему вы можете легко использовать XCTest framework для написания тестов пользовательского интерфейса для ваших приложений, написанных на UIKit или SwiftUI.
Давайте напишем наш первый пользовательский тест для простого представления, отображающего сообщение.
struct ContentView: View {
var body: some View {
Text("Hello World!")
}
}
Чтобы создать новую цель тестирования пользовательского интерфейса, перейдите в меню Xcode File -> New -> Target -> UI Testing bundle. Теперь мы можем написать наш первый пользовательский тест.
import XCTest
final class UITests: XCTestCase {
var app: XCUIApplication!
override func setUp() {
continueAfterFailure = false
app = XCUIApplication()
app.launchArguments = ["testing"]
app.launch()
}
func testWelcomeMessage() {
XCTAssertTrue(app.staticTexts["Hello World!"].exists)
}
}
Чтобы написать тест пользовательского интерфейса, мы должны создать класс Swift, который расширяет XCTestCase. Xcode запускает каждый метод, начинающийся со слова test, как отдельный тест. Как вы можете видеть в примере выше, мы переопределяем метод setUp. Xcode запускает этот метод перед каждым тестом в тестовом примере. Это отличное место для начальной настройки перед каждым тестированием.
Здесь мы запускаем приложение с нуля перед каждым тестированием пользовательского интерфейса. XCTest предоставляет нам класс XCUIApplication в качестве посредника для приложения, указанного в целевой настройке “Target Application”. Мы можем получить доступ к нашему приложению и взаимодействовать с ним через экземпляр класса XCUIApplication.
В нашем простом тесте мы проверяем, что наше приложение отображает статический текст с определенным сообщением.
Запросы
XCUIApplication предоставляет множество свойств для запроса в иерархии представлений. Вы можете получить доступ к кнопкам, меткам, таблицам, слайдерам, переключателям и другим представлениям, расположенным в вашей иерархии представлений. Основным требованием для доступа к представлению является включенный режим доступности (accessibility). По умолчанию поддержка специальных возможностей (accessibility) включена для любого представления.
let email = app.textFields["email"]
let pwd = app.secureTextFields["password"]
let message = app.staticTexts["Hello World!"]
let loginButton = app.buttons["login"]
Как вы можете видеть, мы можем получить доступ ко всем необходимым представлениям, используя синтаксис сабскриптов. Мы передаем строку, чтобы найти элемент управления в иерархии представлений. Xcode пытается найти представление, соответствующее лейблу специальных возможностей (accessibility label) или идентификатору специальных возможностей (accessibility identifier).
Надписи, кнопки и переключатели используют заголовки в качестве меток специальных возможностей (accessibility labels) из коробки. Но нам все равно нужно вручную установить идентификаторы специальных возможностей для таких представлений, как UITableView и UICollectionView.
// UIKit
let tableView = UITableView()
tableView.accessibilityIdentifier = "newsList"
// SwiftUI
List {
Text("news item")
}.accessibilityIdentifier("newsList")
Чтобы узнать больше о свойствах запроса XCUIApplication, ознакомьтесь с документацией по протоколу XCUIElementTypeQueryProvider.
Действия
Фреймворк XCTest предоставляет нам множество функций для взаимодействия с представлениями. Вы можете нажимать, дважды нажимать, смахивать, сжимать и поворачивать (tap, double‑tap, swipe, pinch and rotate) виды.
app.switches["rememberMe"].tap()
app.buttons["login"].doubleTap()
app.buttons["logout"].twoFingerTap()
Чтобы узнать больше о действиях, которые предоставляет нам XCTest, ознакомьтесь с документацией для класса XCUIElement.
Продвинутый пример
Теперь мы можем взаимодействовать с нашим приложением, используя фреймворк XCTest. Давайте напишем более интересный тест, который проверяет процесс входа в систему. Предположим, что у вас есть представление входа в систему, написанное в Swift UI. Это могло бы выглядеть примерно так:
struct LoginView: View {
@ObservedObject var viewModel: ViewModel
var body: some View {
Form {
Section {
TextField("email", text: $viewModel.email)
.textContentType(.emailAddress)
.accessibilityLabel("email")
SecureField("password", text: $viewModel.password)
.textContentType(.password)
.accessibilityLabel("password")
Toggle("rememberMe", isOn: $viewModel.rememberMe)
.accessibilityIdentifier("rememberMe")
}
Button("login", action: viewModel.login)
}
}
}
struct ContentView: View {
@StateObject var viewModel = ViewModel()
var body: some View {
if viewModel.isAuthorized {
Text("Hello World!")
} else {
LoginView(viewModel: viewModel)
}
}
}
Здесь у нас есть Content View , которое представляет LoginView и заменяет его сообщением, как только пользователь вошел в систему.
import XCTest
final class UITests: XCTestCase {
var app: XCUIApplication!
override func setUp() {
continueAfterFailure = false
app = XCUIApplication()
app.launchArguments = ["testing"]
app.launch()
}
func testLoginFlow() {
let email = app.textFields["email"]
email.tap()
email.typeText("cmecid@gmail.com")
let pwd = app.secureTextFields["password"]
pwd.tap()
pwd.typeText("pwd")
app.switches["rememberMe"].tap()
app.buttons["login"].doubleTap()
app.buttons["login"].twoFingerTap()
let message = app.staticTexts["Hello World!"]
XCTAssertTrue(message.waitForExistence(timeout: 5))
}
}
В приведенном выше примере у нас есть тест, который проверяет процесс входа в систему. Пожалуйста, обратите внимание, что здесь мы используем функцию waitForExistence. Он ожидает определенного тайм-аута и возвращает значение false, если элемент не отображается. С другой стороны, он возвращает значение true, как только элемент появляется на экране.
Производительность
UI тесты выполняются медленнее, чем модульные тесты, потому что им необходимо запустить все приложение целиком. Вот почему мы обычно стараемся написать как можно больше модульных тестов и охватить основные пользовательские потоки тестами пользовательского интерфейса. Но все же есть способ повысить производительность, отключив анимацию во время выполнения тестов пользовательского интерфейса.
final class AppDelegate: NSObject, UIApplicationDelegate {
func applicationDidFinishLaunching(_ application: UIApplication) {
if CommandLine.arguments.contains("testing") {
// clear your app state before running UI tests here.
UIView.setAnimationsEnabled(false)
}
}
}
Здесь мы проверяем, запускается ли приложение в рамках теста пользовательского интерфейса, используя аргументы командной строки. В предыдущих примерах мы задавали аргументы запуска во время выполнения тестов пользовательского интерфейса, и именно так мы можем проверить аргументы и подготовить наше приложение к тестам пользовательского интерфейса. Например, мы можем сбросить состояние приложения, удалив настройки пользователя по умолчанию, данные цепочки для ключей и очистив локальную базу данных.
Заключение
Тесты пользовательского интерфейса - это отличный способ проверить наиболее важные пользовательские потоки в вашем приложении. XCTest framework предоставляет очень приятный и простой API, который мы можем использовать для непосредственного написания сложных тестов. Вы можете написать свой первый пользовательский тест просто взаимодействуя с пользовательским интерфейсом вашего приложения.