Одной из замечательных особенностей разработки в SwiftUI является Xcode Previews, которые обеспечивают быструю UI-итерацию путем визуализации изменений кода в режиме реального времени наряду с кодом SwiftUI. В DoorDash мы активно используем Xcode Previews вместе с библиотекой SnapshotTesting от Point-Free, чтобы убедиться, что экраны выглядят так, как мы ожидаем, при их разработке, и гарантировать, что они не изменятся неожиданным образом с течением времени. SnapshotTesting можно использовать для захвата визуализированного изображения VIEW и создания XCTest - сбоя, если новое изображение не соответствует эталонному изображению на диске. Xcode Previews в сочетании с SnapshotTesting можно использовать для обеспечения быстрых итераций, при этом гарантируя, что вью продолжают выглядеть так, как они задуманы, не опасаясь неожиданных изменений.
Трудность совместного использования Xcode Previews и SnapshotTesting заключается в том, что это может привести к большому количеству шаблонов и дублированию кода между превью и тестами. Чтобы решить эту проблему, инженеры DoorDash разработали PreviewSnapshots, инструмент предварительного просмотра снапшотов, с открытым исходным кодом, который можно использовать для простого обмена конфигураций между превью Xcode и тестами снапшотов. В этой статье мы тщательно исследуем эту тему, сначала предоставив некоторые сведения о том, как работают Xcode-превью и SnapshotTesting, а затем объясним, как использовать новый опенсорс-инструмент, с пояснительными примерами того, как исключить дублирование кода с помощью передачи конфигураций вью между превью и снапшотами.
Как работают Xcode Previews
Xcode Previews позволяют разработчикам возвращать одну или несколько версий View из PreviewProvider, а Xcode визуализирует “живую” версию View вместе с кодом реализации.
Начиная с Xcode 14 вью с несколькими превью представлены в виде выбираемых вкладок в верхней части окна превью, как показано на рисунке 1.
Рисунок 1: Редактор Xcode, показывающий код SwiftUI View для отображения простого сообщения, и окно Xcode Preview, отображающее две версии этого вью. Одна с коротким сообщением и одна с длинным сообщением.
Как работает SnapshotTesting
Библиотека SnapshotTesting позволяет разработчикам писать тестовые утверждения о внешнем виде их вью. Утверждая(*заявляя), что вью соответствует эталонным изображениям на диске, разработчики могут быть уверены, что со временем вью не изменятся неожиданным образом.
Пример кода на рис. 2 сравнивает короткую и длинную версии MessageView с эталонными изображениями, хранящимися на диске как testSnapshots.1 и testSnapshots.2 соответственно. Первоначально снапшоты были записаны с помощью SnapshotTesting и автоматически названы по имени тестовой функции и позиции утверждения внутри функции.
Рис. 2. Редактор Xcode, показывающий код SwiftUI View, использующий PreviewSnapshots для создания Xcode Previews для четырех различных состояний ввода, а также окно Xcode Preview, визуализирующее(* отрисовывающее) вью с использованием каждого из этих состояний.
Проблема совместного использования Xcode Previews и SnapshotTesting
Между кодом, используемым для Xcode Previews, и кодом для создания тестов снапшотов есть много общего. Это сходство может привести к дублированию кода и дополнительным усилиям разработчиков в попытке охватить обе технологии. В идеале разработчики могли бы написать код для предпросмотра вью в различных конфигурациях, а затем повторно использовать этот код для тестирования снапшотов вью в тех же самых конфигурациях.
Представляем PreviewSnapshots
PreviewSnapshots может помочь решить эту проблему дублирования кода. PreviewSnapshots позволяет разработчикам создавать единый набор состояний вью для Xcode Previews и создавать примеры тестирования снапшотов для каждого из состояний с одним тестовым утверждением. Ниже мы рассмотрим, как это работает, на простом примере.
Использование PreviewSnapshots для простого вью
Допустим, у нас есть вью, которое принимает список имен и отображает их каким-то интересным нам способом.
Традиционно мы хотели бы создать превью для нескольких интересующих нас состояний данного вью. Может быть: пусто, одно имя, короткий список имен и длинный список имен.
struct NameList_Previews: PreviewProvider {
static var previews: some View {
NameList(names: [])
.previewDisplayName("Empty")
.previewLayout(.sizeThatFits)
NameList(names: [“Alice”])
.previewDisplayName("Single Name")
.previewLayout(.sizeThatFits)
NameList(names: [“Alice”, “Bob”, “Charlie”])
.previewDisplayName("Short List")
.previewLayout(.sizeThatFits)
NameList(names: [
“Alice”,
“Bob”,
“Charlie”,
“David”,
“Erin”,
//...
])
.previewDisplayName("Long List")
.previewLayout(.sizeThatFits)
}
}
Далее мы написали бы очень похожий код для тестирования снапшотов.
final class NameList_SnapshotTests: XCTestCase {
func test_snapshotEmpty() {
let view = NameList(names: [])
assertSnapshot(matching: view, as: .image)
}
func test_snapshotSingleName() {
let view = NameList(names: [“Alice”])
assertSnapshot(matching: view, as: .image)
}
func test_snapshotShortList() {
let view = NameList(names: [“Alice”, “Bob”, “Charlie”])
assertSnapshot(matching: view, as: .image)
}
func test_snapshotLongList() {
let view = NameList(names: [
“Alice”,
“Bob”,
“Charlie”,
“David”,
“Erin”,
//...
])
assertSnapshot(matching: view, as: .image)
}
}
Длинный список имен потенциально может быть распределен между превью и тестированием снапшотов с помощью статического свойства, но при этом избежать написания вручную отдельного теста снапшотов для каждого просматриваемого состояния не получится.
PreviewSnapshots позволяет разработчикам определить единую коллекцию интересующих конфигураций, а затем тривиально повторно использовать их между превью и тестами снапшотов.
Так выглядит Xcode превью с использованием PreviewSnapshots:
struct NameList_Previews: PreviewProvider {
static var previews: some View {
snapshots.previews.previewLayout(.sizeThatFits)
}
static var snapshots: PreviewSnapshots<[String]> {
PreviewSnapshots(
configurations: [
.init(name: "Empty", state: []),
.init(name: "Single Name", state: [“Alice”]),
.init(name: "Short List", state: [“Alice”, “Bob”, “Charlie”]),
.init(name: "Long List", state: [
“Alice”,
“Bob”,
“Charlie”,
“David”,
“Erin”,
//...
]),
],
configure: { names in NameList(names: names) }
)
}
}
Чтобы создать коллекцию PreviewSnapshots, мы создаем экземпляр PreviewSnapshots с массивом конфигураций вместе с функцией configure для настройки вью для данной конфигурации. Конфигурация состоит из имени и экземпляра State, которое будет использоваться для настройки представления. В этом случае тип состояния для массива имен будет [String].
Для создания превью мы возвращаем snapshots.previews из стандартного статического свойства превью, как показано на рис. 3. snapshots.previews создаст превью с правильным именем для каждой конфигурации PreviewSnapshots.
Рис. 3. Редактор Xcode, показывающий код SwiftUI View с использованием PreviewSnapshots для генерации Xcode Previews для четырех различных состояний ввода вместе с окном Xcode Preview, отображающим вью с использованием каждого из этих состояний.
PreviewSnapshots обеспечивает некоторую дополнительную структуру для небольшого вью, построить которое несложно, но мало что делает для сокращения количества строк кода в превью. Основное преимущество небольших вью проявляется, когда приходит время писать тесты снапшотов для превью.
final class NameList_SnapshotTests: XCTestCase {
func test_snapshot() {
NameList_Previews.snapshots.assertSnapshots()
}
}
Это единственное утверждение выполнит снапшот-тест каждой конфигурации в PreviewSnapshots. На рис. 4 показан код примера вместе с эталонными изображениями в Xcode. Кроме того, если в превью будут добавлены какие-либо новые конфигурации, к ним автоматически будет применен снапшот-тест без изменения тестового кода.
Рисунок 4: Модульный тест Xcode с использованием PreviewSnapshots для проверки четырех различных состояний ввода, определенных выше, с помощью одного вызова assertSnapshots
Для более сложных вью с большим количеством аргументов - еще больше преимуществ.
Использование PreviewSnapshots для более сложного вью
Во втором примере мы рассмотрим FormView, который принимает несколько Bindings, опциональное сообщение об ошибке и замыкание действия в качестве аргументов в своем инициализаторе. Этот пример покажет возросшие преимущества PreviewSnapshots в ситуации, когда увеличивается сложность построения вью.
struct FormView: View {
init(
firstName: Binding
lastName: Binding
email: Binding
errorMessage: String?,
submitTapped: @escaping () -> Void
) { ... }
// ...
}
Поскольку PreviewSnapshots является дженериком для состояния ввода, мы можем объединить различные входные параметры в небольшую вспомогательную структуру для передачи в блок configure, и лишь один раз нужно будет cоставить FormView. В качестве дополнительного удобства PreviewSnapshots предоставляет протокол NamedPreviewState для упрощения создания конфигураций входа путем группировки имени превью вместе(*в соответствии) с состоянием превью.
struct FormView_Previews: PreviewProvider {
static var previews: some View {
snapshots.previews
}
static var snapshots: PreviewSnapshots
PreviewSnapshots(
states: [
.init(name: "Empty"),
.init(
name: "Filled",
firstName: "John", lastName: "Doe", email: "john.doe@doordash.com"
),
.init(
name: "Error",
firstName: "John", lastName: "Doe", errorMessage: "Email Address is required"
),
],
configure: { state in
NavigationView {
FormView(
firstName: .constant(state.firstName),
lastName: .constant(state.lastName),
email: .constant(state.email),
errorMessage: state.errorMessage,
submitTapped: {}
)
}
}
)
}
struct PreviewState: NamedPreviewState {
let name: String
var firstName: String = ""
var lastName: String = ""
var email: String = ""
var errorMessage: String?
}
}
В коде примера мы создали структуру PreviewState, соответствующую по форме NamedPreviewState и содержащую имя превью наряду с именем, фамилией, адресом электронной почты и опциональным сообщением об ошибке для создания вью. Затем, на основе переданного состояния конфигурации, в блоке configure мы создаем один экземпляр FormView. Возвращая snapshots.preview из PreviewProvider.previews, PreviewSnapshots будет перебирать входные состояния и создаст превью Xcode с надлежащим именем для каждого состояния, как показано на рисунке 5.
Рисунок 5: Редактор Xcode, показывающий код SwiftUI View с использованием PreviewSnapshots для создания превью Xcode для трех различных состояний ввода вместе с окном Xcode Preview, визуализирующим вью с использованием каждого из этих состояний.
После того, как мы определили набор PreviewSnapshots для превью, мы снова можем создать набор снапшот-тестов с единственным утверждением юнит-теста.
final class FormView_SnapshotTests: XCTestCase {
func test_snapshot() {
FormView_Previews.snapshots.assertSnapshots()
}
}
Как и в приведенном выше более простом примере, этот тестовый пример будет сравнивать каждое из состояний превью, определенных в FormView_Previews.snapshots, с эталонным изображением, записанным на диск, и генерировать сбой теста, если изображения не соответствуют ожиданиям.
Заключение
В этой статье обсуждались определенные преимущества использования Xcode Previews и SnapshotTesting при разработке с помощью SwiftUI. Он также продемонстрировал некоторые болевые точки и дублирование кода, которые могут возникнуть в результате совместного использования этих двух технологий, и то, как PreviewSnapshots позволяет разработчикам сэкономить время, повторно используя усилия, которые они вложили в написание превью Xcode для тестирования снапшотов.
Инструкции по включению PreviewSnapshots в ваш проект, а также пример приложения, использующего PreviewSnapshots, доступны на GitHub.