Как ускорить разработку и тестирование в SwiftUI с помощью PreviewSnapshots

от автора

Одной из замечательных особенностей разработки в 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, отображающее две версии этого вью. Одна с коротким сообщением и одна с длинным сообщением.
Рисунок 1: Редактор Xcode, показывающий код SwiftUI View для отображения простого сообщения, и окно Xcode Preview, отображающее две версии этого вью. Одна с коротким сообщением и одна с длинным сообщением.

Как работает SnapshotTesting

Библиотека SnapshotTesting позволяет разработчикам писать тестовые утверждения о внешнем виде их вью. Утверждая(*заявляя), что вью соответствует эталонным изображениям на диске, разработчики могут быть уверены, что со временем вью не изменятся неожиданным образом.

Пример кода на рис. 2 сравнивает короткую и длинную версии MessageView с эталонными изображениями, хранящимися на диске как testSnapshots.1 и testSnapshots.2 соответственно. Первоначально снапшоты были записаны с помощью SnapshotTesting и автоматически названы по имени тестовой функции и позиции утверждения внутри функции.

Рис. 2. Редактор Xcode, показывающий код SwiftUI View, использующий PreviewSnapshots для создания Xcode Previews для четырех различных состояний ввода, а также окно Xcode Preview, визуализирующее(* отрисовывающее) вью с использованием каждого из этих состояний.
Рис. 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, отображающим вью с использованием каждого из этих состояний.
Рис. 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
Рисунок 4: Модульный тест Xcode с использованием PreviewSnapshots для проверки четырех различных состояний ввода, определенных выше, с помощью одного вызова assertSnapshots

Для более сложных вью с большим количеством аргументов — еще больше преимуществ.

Использование PreviewSnapshots для более сложного вью

Во втором примере мы рассмотрим FormView, который принимает несколько Bindings, опциональное сообщение об ошибке и замыкание действия в качестве аргументов в своем инициализаторе. Этот пример покажет возросшие преимущества PreviewSnapshots в ситуации, когда увеличивается сложность построения вью.

struct FormView: View {   init(     firstName: Binding<String>,     lastName: Binding<String>,     email: Binding<String>,     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<PreviewState> {     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, визуализирующим вью с использованием каждого из этих состояний.
Рисунок 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.


ссылка на оригинал статьи https://habr.com/ru/post/713346/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *