Автоматическое тестирование визуального интерфейса (UI-тесты) имеет несколько проблем. Первая проблема — добиться того, чтобы UI-тесты были стабильными и не падали от запуска к запуску по независящим от самого кода причинам. Например, если сетевой запрос завис, то визуальный компонент не обновится вовремя, произойдет тайм-аут и тест упадет.

Вторая проблема — тестопригодность: добиться изоляции визуального компонента от сетевых сервисов, аппаратных сервисов (например, геолокации), а также обеспечить легкость введения компонента в нужные состояния.

Будем различать несколько видов тестов:

  • системный тест (system test, end-to-end test, e2e) — тестирование всей системы в целом;
  • модульный тест (unit test, component test) — тестирование отдельно взятого компонента.

В данной статье под объектом тестирования рассматривается визуальный компонент, который будет тестироваться через пользовательский интерфейс. Предмет тестирования — функциональная корректность объекта тестирования. Таким образом, мы рассматриваем модульный тест визуального компонента в изоляции от других его зависимостей.

Объект тестирования

Рассмотрим визуальный компонент “Подсказка” (SearchVC), который реализует следующие пользовательские сценарии:

  1. Компонент подсказывает результаты поиска по мере ввода ключевого слова.
  2. Пользователь выбирает элемент из результатов поиска.
  3. Пользователь выбирает ключевое слово из строки поиска.

Архитектурно компонент SearchVC имеет следующие зависимости:

  • делегат SearchVCDelegate
  • сервис поиска подсказок SuggestionService

Рис. 1. Диаграмма классов компонента SearchVC

Интерфейс SearchVCDelegate содержит только существенные для сценария события:

  1. Ввод ключевого слова.
  2. Выбор элемента из результатов поиска.

Интерфейс 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)
    }
}

Рис. 2. Интерфейс со списком всех тестируемых компонентов

Такой подход позволяет запустить любой визуальный компонент в один шаг со всеми необходимыми для тестирования зависимостями, которые определит инженер в коде. Таким образом решается проблема тестопригодности, когда необходимо проделывать сложную процедуру введения приложения в тестируемое состояние: прокликивание приложения до нужного визуального компонента.

Кроме этого, такое приложение может служить хорошей демонстрацией всех доступных визуальных компонентов в приложении:

  • для ручного тестирования;
  • для демонстрации возможностей;
  • для поиска переиспользуемых компонентов другими программистами.

Применение тестовых двойников

Воспользуемся шаблоном “Тестовый двойник” для изоляции зависимости от SuggestionService. Тестовый двойник [Месарош, 2009] — такой компонент, который эквивалентный заменяемому, но пригодный к тестированию.

Рис. 3. Диаграмма классов компонента “Тестовый двойник”

Функция тестового двойника будет такой: в качестве параметра конструктора он примет список слов, а в блок 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)

Рис. 4. Запуск автоматического теста интерфейса пользователя.

Заключение

В статье рассмотрен подход к модульному тестированию визуальных компонент, который позволяет исключить влияние зависимостей (аппаратные, сетевые и другие сервисы) на результаты тестирования.

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

Библиография

Месарош, Джерард. Шаблоны тестирования 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 лет опыта преподавателем в ХНУРЭ.