Как и зачем мы внедрили Snapshot Testing
Привет, Хабр!
Меня зовут Никита. Я iOS Teamlead в Московском кредитном банке.
В этой статье расскажу про то, как мы пришли к snapshot-тестам и теперь их используем на своем проекте.
Статья будет полезна как для iOS-разработчиков, так и для iOS-автоматизаторов.
Здесь мы разберем:
Что такое, как работает и для чего нужно snapshot-тестирование
Какие цели мы преследовали
Как внедрить snapshot-тестирование к себе в проект
Начнем сначала немного с теории для нахождения ответов на вопросы: для чего это нам нужно и как можно использовать snapshot-тестирование.
Что такое snapshot-тестирование
С помощью snaphot-тестированиямы можем узнать изменился ли в целом наш интерфейс в процессе изменения кода существующего функционала за счет эталонного изображения (снапшота), которое было сформировано с помощью snapshot-тестов. Путем сравнения эталонного и полученного снапшота. Быстрый способ понять, что изменения в коде не повлекли к изменениям UI-интерфейса.
Snapshot-тесты формируют эталонное изображение экрана и сравнивают полученное изображение с эталонным, если не новый функционал. Забегу немного вперед: мы не стали писать snapshot-тесты на все подряд, а только на Success-ответ запроса (если имеется) для проверки верстки экрана и общих UI-компонентов.
Как работает snapshot-тестирование
Мы тестируем UI-интерфейс и хотим убедиться, что он остается неизменным. Начинаем snapshot-тестирование с выбора UI-интерфейса в определенном состоянии для формирования эталона. Во время последующих прогонов snapshot-тестов текущий UI-интерфейс сравнивается с этим сохраненным эталоном.
Если они совпадут, то значит изменений нет и все хорошо. Но если snapshot-тест завершится неудачей, то есть возникают расхождения, указывающие неожиданное изменение UI-интерфейса. Данный метод эффективен для поддержки визуальной целостности и выявления проблем с UI-интерфейсом, обеспечивающих безупречный пользовательский опыт.
Преимущества snapshot-тестирования
Экономит время разработчиков за счет простых тестов, при этом без особого поддерживая их в актуальном состоянии;
Улучшает отслеживание изменений, сокращает количество ошибок и обеспечивает стабильность приложения:
Обеспечивает целостность кода и как следствие — эффективную поставку качественного продукта;
Легко настроить и внедрить;
Обнаруживает непреднамеренные визуальные изменения.
Для себя лично нашел много преимуществ: автоматизация review на различных устройствах; новый вид тестов; уменьшение количества визуальных ошибок; скорость прохождение snapshot тестов, но все перечислять не буду, так как думаю, что каждый найдет для себя те или иные преимущества у snapshot-тестирования. Свои минусы само собой есть, к примеру: не стабильность из за разных процессоров (intel и m1), нельзя проверить анимацию.
Какие цели преследовали
Уменьшение ошибок, связанных с версткой общими компонентами;
Ускорение проведение code review на визуальное качество;
Проверка визуального отображение на нескольких устройствах с разным расширением дисплея.
Бизнес-ценность
Время исполнения и разработки практически аналогично Unit-тестированию;
Не затрагивает основную кодовую базу проекта;
Разрабатывает сам разработчик;
Наилучший способ автоматизированной защиты/проверки/контроля целостности экранов пользовательского интерфейса в соотношении «цена/качество».
Какие есть библиотеки
При поиске решения нашел всего 2 библиотеки для закрытия задачи:
Сводная таблица:
SnapshotTesting | iOSSnapshotTestCase | |
Язык реализации | Swift | Objective-C |
Diff-скриншоты | Есть | Есть |
Гибкая настройка погрешности совпадения скриншотов | 1 параметр | 2 параметра |
Поддержка ОS | iOS, macOS, visionOS, tvOS. watchOS, Linux | iOS (возможно, что есть еще OS, но информации не нашел) |
Поддерживает менеджеры зависимостей | CocoaPods, Carthage, Swift Package Manager | CocoaPods, Carthage, Swift Package Manager |
Скриншоты любого UI-компонента | Есть | Нужно реализовывать самому |
Последняя дата обновления | 13 октября 2023 | 22 октября 2021 |
Возможно, что библиотек больше, но все нужные потребности закрыла библиотека — SnapshotTesting.
Как внедрить snapshot-тесты
Перед тем как начать реализовывать — нам потребуется:
Добавить target-тесты в проект;
Выбрать один из вариантов установки библиотеки SnapshotTesting.
Реализация
На момент внедрения snapshot-тестов мы имели на проекте архитектуру VIPER и для unit- тестов готовый механизм по мокированию запросов (с помощью макросов, разветвления кода и наличия локальных файлов, имитирующих ответ на запрос), который нам очень сильно помог ускорить скорость написания snapshot-тестов. Однако наличие этих пунктов необязательно — у каждого своего проекта есть свой подход.
После того как выполнили все шаги из пункта «Как внедрить snapshot-тесты», нам требуется импортировать подключенную библиотеку для получения доступа к ней — import SnapshotTesting.
Дальше нам требуется реализовать snapshot-тест:
Создаем метод теста, например, func testSnapshotIphoneXr (). Мы выбрали для себя 3 устройства (iPhone Xr, iPhone Se, iPhone 8), но библиотека позволяет тестировать на большом количестве.
Дальше нам нужно сконфигурировать наш экран и получишь его ViewController, у нас это метод screenConfigutation ().
Обязательным условием для snapshot-тестов является запуск жизненного цикла ViewController — у нас это presenter.present (from: UIViewController ()), который презентует нам экран.private func screenConfiguration() -> UIViewController { let view: CalculatorViewInput = CalculatorViewController.create() view.output = presenter presenter.view = view presenter.present(from: UIViewController()) return view.viewController }
Теперь вызовем метод для формирования эталонного изображения (метод же отвечает и за валидацию в дальнейшем) у SnapshotTesting — assertSnapshots.
На выходе получаем:
func testSnapshotIphone8() {
let vc = screenConfiguration()
assertSnapshots(matching: vc,
as: [.image(on: iPhone8, precision: 1)],
record: false,
testName: "iPhone 8 @\(Int(UIScreen.main.scale))x”)
}
func testSnapshotIphoneSe() {
let vc = screenConfiguration()
assertSnapshots(matching: vc,
as: [.image(on: iPhoneSe, precision: 1)],
record: false,
testName: "iPhone Se @\(Int(UIScreen.main.scale))x”)
}
func testSnapshotIphoneXr() {
let vc = screenConfiguration()
assertSnapshots(matching: vc,
as: [.image(on: iPhoneXr, precision: 1)],
record: false,
testName: "iPhone Xr @\(Int(UIScreen.main.scale))x”)
}
Запускаем наш тест, при первом запуске он завершится fail, так как у нас формируется эталонное изображение. Запускаем повторно, теперь success. Сформированные снапшоты можно посмотреть по пути нашего теста в проекте в папке __Snapshots__.
Параметры
Теперь поговорим о параметрах assertSnapshots из примеров:
Matching — сюда мы передаем наш ViewController для того, чтобы assertSnapshots понимал, у какого именно экрана нужно сформировать/провалидировать снапшоты;
As — массив, в который мы кладем «действие которое нам нужно совершить», а в нашем случае это либо image (отвечает за формирование снапшота всего экрана), либо wait (формирование снапшота с заданной задержкой если имеется запрос), либо нужно подождать какого — то определенного действия:
Image: on — на каком устройстве требуется сформировать/провалидировать снапшот и precision — процент совпадения. В данном случае 1 это 100% совпадение — идеальное значение. Также вместо конкретного устройства можно задать свой size устройства и установить safeArea.
func testSnapshotIphone8() { let vc = screenConfiguration() assertSnapshots(matching: vc, as: [.image(on: .init(safeArea: .init(top: 20, left: 0, bottom: 0, right: 0), size: .init(width: 375, height: 1000), traits: .init()), precision: 1)], record: false, testName: "iPhone 8 @(Int(UIScreen.main.scale))x”) }
Wait: for — сколько по времени нам нужна задержка, on — что мы хотим после задержки — в нашем случае сформировать снапшот всего экрана.
func testSnapshotIphone8() { let vc = screenConfiguration() assertSnapshots(matching: vc, as: [.image(on: iPhone8, precision: 1), .wait(for: 0,1, on: .image(on: iPhone8, precision: 1))], record: false, testName: "iPhone 8 @(Int(UIScreen.main.scale))x”) }
Почему в параметре as 2 элемента, рассмотрим вариант с запросом — 1 отвечает за момент загрузки экрана, а 2 за уже отображения отрисованного экрана после получения ответа на запрос. И для примера покажу, как изменился screenConfiguration (). Мы добавляем собственный механизм мокирования запросов, который нам возьмет json файл из проекта.
На выходе получаем такой результат:
private func screenConfiguration() -> UIViewController { let view: CalculatorViewInput = CalculatorViewController.create() view.output = presenter presenter.view = view UnitTestManager.sharedInstance.nameTest = "testSnapshot” UnitTestManager.sharedInstance.startSession() presenter.present(from: UIViewController()) return view.viewController }
Record — флаг, с помощью которого можем решить нужно переписать существующие снапшоты или нет в рамках нашего теста, но также можем использовать глобальную переменную isRecording.
TestName — имя нашего снапшота.
Все параметры не буду рассматривать, так как их достаточно большое количество, с ним можно ознакомиться на SnapshotTesting.
Результаты
Теперь разберем ситуацию, когда у нас тест завершился fail при каких-либо изменениях на экране. У нас после прогона теста сформируются 3 снапшота: reference, failure и difference.
Reference
Failure
Difference
На примере можем увидеть, что отступ изменился. О чем нам говорит difference-снапшот.
Для нас это означает, что был затронут общий компонент интерфейса или изменился интерфейс, и это не ошибка. В данном случае 1-й вариант.
И еще один пример для большего понимания:
Reference
Failure
Difference
Итог
Мы узнали, что такое snapshot-тестирование, как внедрить к себе в проект и, конечно, писать snaphot-тесты. Цели который были поставлены, мы закрыли: с помощью snapshot-тестов уменьшили количество ошибок, связанных с версткой, ускорили процесс code review и автоматизировали проверку отображения на нескольких устройствах с разным расширением дисплея. Можно пойти дальше и внедрить данный механизм в CI/CD в разрез pipelines из-за хорошего времени прохождения, у нас прохождение каждого snapshot-теста занимает в среднем меньше 1 секунды.
Из подводных камней заметил, что если запускать snapshot-тесты на разных симуляторах, отличные от симулятора, на котором были сформированы эталонные снапшоты, то тесты будут fail.
Надеюсь, что статья оказалась для вас полезной, спасибо за внимание. Если у вас был или вы хотите поделиться своим опытом в snapshot-тестировании, напишите, пожалуйста, в комментариях, а я постараюсь ответить.