Автоматическое модульное тестирование визуальных компонентов в iOS
Автоматическое тестирование визуального интерфейса (UI-тесты) имеет несколько проблем. Первая проблема — добиться того, чтобы UI-тесты были стабильными и не падали от запуска к запуску по независящим от самого кода причинам. Например, если сетевой запрос завис, то визуальный компонент не обновится вовремя, произойдет тайм-аут и тест упадет.
Вторая проблема — тестопригодность: добиться изоляции визуального компонента от сетевых сервисов, аппаратных сервисов (например, геолокации), а также обеспечить легкость введения компонента в нужные состояния.
Будем различать несколько видов тестов:
- системный тест (system test, end-to-end test, e2e) — тестирование всей системы в целом;
- модульный тест (unit test, component test) — тестирование отдельно взятого компонента.
В данной статье под объектом тестирования рассматривается визуальный компонент, который будет тестироваться через пользовательский интерфейс. Предмет тестирования — функциональная корректность объекта тестирования. Таким образом, мы рассматриваем модульный тест визуального компонента в изоляции от других его зависимостей.
Объект тестирования
Рассмотрим визуальный компонент “Подсказка” (SearchVC), который реализует следующие пользовательские сценарии:
- Компонент подсказывает результаты поиска по мере ввода ключевого слова.
- Пользователь выбирает элемент из результатов поиска.
- Пользователь выбирает ключевое слово из строки поиска.
Архитектурно компонент SearchVC имеет следующие зависимости:
- делегат SearchVCDelegate
- сервис поиска подсказок SuggestionService
Интерфейс SearchVCDelegate содержит только существенные для сценария события:
- Ввод ключевого слова.
- Выбор элемента из результатов поиска.
Интерфейс SuggestionService содержит метод, который принимает параметр query — запрос на подсказку, а также completion — функцию в которую сервис передаст результаты поиска или ошибку.
Предмет тестирования
Нас могут интересовать множество аспектов работы объекта тестирования: функциональная корректность, производительность, отказоустойчивость, безопасность. Под предметом тестирования понимается тот единственный аспект, который нас интересует в данный момент.
В этой статье предмет тестирования — это функциональная корректность компонента.
Решение
В Xcode-проекте создается отдельный target под названием UIDemo, в котором мы будем тестировать визуальные компоненты в изоляции. В этом таргете создается типичное master-detail приложение, в котором каждый элемент — это тестируемый UIViewController.
Для удобства и расширяемости введена коллекция viewControllers, которая содержит пары: имя компонента и фабрика, которая порождает экземпляр компонента со всеми необходимыми зависимостями. Добавление новых визуальных компонентов для тестирования достигается простым дописыванием новых пар в коллекцию.
Листинг 1. Реализация списка со всеми доступными визуальными компонентами
class MasterViewController: UITableViewController {
// Collection of UIViewControllers under test.
// Implemented as pairs of titles and view-controller factories.
var viewControllers : [(String, () -> UIViewController)] = [
("SearchVC", {
let fakeSuggestService = FakeSuggestService(words: [
"Apple",
"Apricot",
"Avocado",
"Banana",
"Blackberries",
"Blackcurrant",
"Blueberries",
"Breadfruit"
])
let searchViewController = DNTLSearchViewController()
searchViewController.suggestionService = fakeSuggestService
return searchViewController
})
]
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return viewControllers.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
cell.textLabel!.text = viewControllers[indexPath.row].0
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let vcFactory = viewControllers[indexPath.row].1
let vc = vcFactory()
self.navigationController?.pushViewController(vc, animated: true)
}
}
Такой подход позволяет запустить любой визуальный компонент в один шаг со всеми необходимыми для тестирования зависимостями, которые определит инженер в коде. Таким образом решается проблема тестопригодности, когда необходимо проделывать сложную процедуру введения приложения в тестируемое состояние: прокликивание приложения до нужного визуального компонента.
Кроме этого, такое приложение может служить хорошей демонстрацией всех доступных визуальных компонентов в приложении:
- для ручного тестирования;
- для демонстрации возможностей;
- для поиска переиспользуемых компонентов другими программистами.
Применение тестовых двойников
Воспользуемся шаблоном “Тестовый двойник” для изоляции зависимости от SuggestionService. Тестовый двойник [Месарош, 2009] — такой компонент, который эквивалентный заменяемому, но пригодный к тестированию.
Функция тестового двойника будет такой: в качестве параметра конструктора он примет список слов, а в блок completion передаст только те, которые начинаются с подстроки, указанной в query. Таким образом мы добиваемся управляемости: в тестах мы всегда знаем на какие ключевые слова какой результат ожидать от тестового двойника.
class FakeSuggestService: SuggestService {
var words: [String]
init(words: [String]) {
self.words = words
}
func suggest(query: String, completion: ([String], Error?) -> Void) {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in
completion(self!.words.filter { $0.starts(with: query) }, nil)
}
}
}
В реализации метода suggest тестовой двойник вызывает функцию completion асинхронно с задержкой 200 мс, чтобы имитировать задержку ответа от сервера. Это позволит сделать тестовый двойник более адекватным реальности.
Внедрение зависимостей
Жесткая зависимость одного компонента от другого — это типичный признак нетестопригодного кода, который приводит к отказу от автоматического тестирования [Обризан, 2019]. В случае визуальных компонентов с зависимостью от сетевых сервисов это приводит к тому, что зачастую сложно предугадать состояние сервера: какие данные там есть, какие будут ответные реакции на тестовые воздействия. Кроме этого, на результаты тестирования может повлиять состояние сетевого соединения (задержка, обрыв).
Программируйте в соответствии с интерфейсом, а не с реализацией! (Эрих Гамма)
Внедрение зависимостей (dependency injection) [Фаулер, 2004] — метод тестопригодного проектирования архитектуры приложения, при котором зависимость одного объекта от другого может быть подменена во время компиляции (build time) или работы приложения (run time).
Существует четыре способа внедрения зависимостей:
- через конструктор класса;
- через свойство объекта;
- через метод объекта;
- через ServiceLocator.
В нашем решении мы воспользовались внедрением зависимости через свойство объекта.
Листинг 3. Внедрение зависимости через свойство объекта.
let fakeSuggestService = FakeSuggestService(words: [
"Apple",
"Apricot",
"Avocado",
"Banana",
"Blackberries",
"Blackcurrant",
"Blueberries",
"Breadfruit"
])
let searchViewController = DNTLSearchViewController()
searchViewController.suggestionService = fakeSuggestService
return searchViewController
Существуют и более продвинутые технологии для внедрения зависимостей. Например, Swinject [Swinject] — технология, которая позволяет реализовать метод внедрения зависимостей в проектах на языке Swift.
Тестовый план
Для подачи тестовых воздействий на объект тестирования были выбран стандартный механизм Apple iOS: классы XCTest и XCUIApplication [Apple].
Таблица 1. План тестов компонента SearchVC.
# | Воздействие | Ожидание |
---|---|---|
1 | A | Присутствует: Apple, Apricot, Avocado Отсутствует: Banana |
2 | Ap | Присутствует: Apple, Apricot Отсутствует: Avocado |
3 | App | Присутствует: Apple Отсутствует: Apricot |
4 | Appo | Присутствует: Item coming soon! Tap to add. Отсутствует: Apple |
Переносим тестовый план в код автоматического теста.
Листинг 4. Текст автоматического теста (фрагмент).
XCTAssertFalse(app!.staticTexts["Apple"].exists)
// Step 1.
app!.searchFields["Search categories"].typeText("A")
XCTAssertTrue(app!.staticTexts["Apple"].exists)
XCTAssertTrue(app!.staticTexts["Apricot"].exists)
XCTAssertTrue(app!.staticTexts["Avocado"].exists)
XCTAssertFalse(app!.staticTexts["Banana"].exists)
// Step 2.
app!.searchFields["Search categories"].typeText("p")
waitForAbsence(element: app!.staticTexts["Avocado"])
XCTAssertTrue(app!.staticTexts["Apple"].exists)
XCTAssertTrue(app!.staticTexts["Apricot"].exists)
// Step 3.
app!.searchFields["Search categories"].typeText("p")
XCTAssertTrue(app!.staticTexts["Apple"].exists)
waitForAbsence(element: app!.staticTexts["Apricot"])
// Step 4.
app!.searchFields["Search categories"].typeText("o")
waitForAbsence(element: app!.staticTexts["Apple"])
XCTAssertTrue(app!.tables.staticTexts["Item coming soon! Tap to add."].exists)
Заключение
В статье рассмотрен подход к модульному тестированию визуальных компонент, который позволяет исключить влияние зависимостей (аппаратные, сетевые и другие сервисы) на результаты тестирования.
Программирование на интерфейсах, внедрение зависимостей и тестовые двойники существенно повышают тестопригодность приложений, позволяют сократить время на разработку и обслуживание тестов, добиться повторяемости результатов тестирования.
Библиография
Месарош, Джерард. Шаблоны тестирования xUnit: рефакторинг кода тестов. : Пер. с англ. — М. : ООО ‘‘И.Д. Вильямс’’, 2009. — 832 с. : ил. — Парал. тит. англ.
Fowler, Martin. Inversion of Control Containers and the Dependency Injection pattern. — Мартин Фаулер, 2004 — Получено 31.05.2020.
Обризан, Владимир. Сопротивления автоматизации тестирования. — Design and Test Lab, 2019 — Получено 31.05.2020.
Обризан, Владимир. Конспект эксперт-лекций “Надежное программное обеспечение”, 2018.
Swinject. Dependency injection framework for Swift with iOS/macOS/Linux. — Получено 21.06.2020.
User Interface Tests. Apple Developer Documentation. — Получено 21.06.2020.
Об авторе

Владимир Обризан, к. т. н.
Консультант CEO и собственников IT-компаний. Директор и основатель Первого института надежного программного обеспечения. Директор и сооснователь IT-компании DESIGN AND TEST LAB. 14 лет опыта разработки, ТОП-менеджмента, и создания успешного IT-бизнеса. 10 лет опыта преподавателем в ХНУРЭ.