Ir al contenido
8953

Blog


Cómo acelerar el desarrollo y las pruebas de SwiftUI utilizando PreviewSnapshots

18 de enero de 2023

|
John Flanagan

John Flanagan

Una de las grandes características de desarrollar en SwiftUI es Vista previa de Xcode que permiten una rápida iteración de la interfaz de usuario mediante la representación de los cambios de código casi en tiempo real junto con el código SwiftUI. En DoorDash hacemos un uso intensivo de Xcode Previews junto con el SnapshotTesting biblioteca de Sin puntos para asegurarnos de que las pantallas tienen el aspecto que esperamos al desarrollarlas y de que no cambian de forma inesperada con el tiempo. SnapshotTesting puede utilizarse para capturar una imagen renderizada de una View y crear un XCTest si la nueva imagen no coincide con la imagen de referencia en disco. Las vistas previas de Xcode en combinación con SnapshotTesting se pueden utilizar para proporcionar iteraciones rápidas al tiempo que se garantiza que las vistas siguen teniendo el aspecto previsto sin temor a cambios inesperados. 

El reto de utilizar Xcode Previews y SnapshotTesting juntos es que puede resultar en una gran cantidad de repeticiones y duplicación de código entre las vistas previas y las pruebas. Para resolver este problema, DoorDash engineering ha desarrollado PreviewSnapshots, una herramienta de snapshot de código abierto que se puede utilizar para compartir fácilmente configuraciones entre las vistas previas de Xcode y las pruebas de snapshot. En este artículo, vamos a profundizar en este tema proporcionando primero algunos antecedentes sobre cómo funcionan las vistas previas de Xcode y SnapshotTesting y luego explicando cómo utilizar la nueva herramienta de código abierto con ejemplos ilustrativos de cómo eliminar la duplicación de código compartiendo configuraciones de vista entre vistas previas e instantáneas.

Cómo funcionan las vistas previas de Xcode

Vista previa de Xcode permiten a los desarrolladores devolver una o más versiones de un View de un PreviewProvider y Xcode mostrará una versión en vivo del archivo View junto con el código de aplicación.

A partir de Xcode 14, las vistas con múltiples previsualizaciones se presentan como pestañas seleccionables en la parte superior del lienzo de previsualización, como se muestra en la Figura 1.

Figura 1: Editor de Xcode mostrando el código de la vista SwiftUI para mostrar un mensaje simple junto con el lienzo de vista previa de Xcode mostrando dos versiones de esa vista. Una con un mensaje corto y otra con un mensaje largo.
Figura 1: Editor de Xcode mostrando el código de la vista SwiftUI para mostrar un mensaje simple junto con el lienzo de vista previa de Xcode mostrando dos versiones de esa vista. Una con un mensaje corto y otra con un mensaje largo.

Cómo funciona SnapshotTesting

La biblioteca SnapshotTesting permite a los desarrolladores escribir afirmaciones de prueba sobre la apariencia de sus vistas. Al afirmar que una vista coincide con las imágenes de referencia en disco, los desarrolladores pueden estar seguros de que las vistas no cambian de forma inesperada con el tiempo.

El código de ejemplo de la Figura 2 comparará las versiones corta y larga de MessageView con las imágenes de referencia almacenadas en disco como testSnapshots.1 y testSnapshots.2 respectivamente. Las instantáneas fueron grabadas originalmente por SnapshotTesting y se le asigna automáticamente el nombre de la función de prueba junto con la posición de la aserción dentro de la función.

Figura 2: Editor Xcode mostrando código SwiftUI View usando PreviewSnapshots para generar Xcode Previews para cuatro diferentes estados de entrada junto con Xcode Preview canvas renderizando la vista usando cada uno de esos estados
Figura 2: Editor Xcode mostrando código SwiftUI View usando PreviewSnapshots para generar Xcode Previews para cuatro diferentes estados de entrada junto con Xcode Preview canvas renderizando la vista usando cada uno de esos estados

El problema de utilizar Xcode Previews y SnapshotTesting juntos

Hay mucho en común entre el código utilizado para las vistas previas de Xcode y para la creación de pruebas instantáneas. Esta similitud puede dar lugar a la duplicación de código y el esfuerzo adicional para los desarrolladores al tratar de adoptar ambas tecnologías. Lo ideal sería que los desarrolladores pudieran escribir código para previsualizar una vista en una variedad de configuraciones y luego reutilizar ese código para realizar pruebas instantáneas de la vista en esas mismas configuraciones.

Presentación de PreviewSnapshots

Resolver este reto de duplicación de código es donde PreviewSnapshots puede ayudar. PreviewSnapshots permite a los desarrolladores crear un único conjunto de estados de vista para las vistas previas de Xcode y crear casos de prueba de instantáneas para cada uno de los estados con una única aserción de prueba. A continuación veremos cómo funciona con un ejemplo sencillo. 

Uso de PreviewSnapshots para una vista simple

Supongamos que tenemos una vista que recibe una lista de nombres y los muestra de alguna forma interesante.

Tradicionalmente querríamos crear una vista previa para algunos estados interesantes de la vista. Tal vez vacía, un solo nombre, una lista corta de nombres y una lista larga de nombres.

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)
  }
}

Luego escribiríamos un código muy similar para las pruebas de instantáneas.

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)
  }
}

La larga lista de nombres podría compartirse potencialmente entre las vistas previas y las pruebas instantáneas utilizando una propiedad estática, pero no se puede evitar escribir manualmente una prueba instantánea individual para cada estado que se esté previsualizando.

PreviewSnapshots permite a los desarrolladores definir una única colección de configuraciones interesantes, y luego reutilizarlas trivialmente entre vistas previas y pruebas de instantáneas.

Este es el aspecto de una vista previa de Xcode usando 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) }
    )
  }
}

Para crear una colección de PreviewSnapshots construimos una instancia de PreviewSnapshots con un array de configuraciones junto con una variable configure para establecer la vista de una configuración determinada. Una configuración consta de un nombre, junto con una instancia de State que se utilizará para configurar la vista. En este caso, el tipo de estado es [String] para la matriz de nombres.

Para generar las previsualizaciones devolvemos snapshots.previews de la norma previews como se ilustra en la Figura 3. snapshots.previews generará una vista previa con el nombre adecuado para cada configuración del PreviewSnapshots.

Figura 3: Editor Xcode mostrando código SwiftUI View usando PreviewSnapshots para generar Xcode Previews para cuatro diferentes estados de entrada junto con Xcode Preview canvas renderizando la vista usando cada uno de esos estados
Figura 3: Editor Xcode mostrando código SwiftUI View usando PreviewSnapshots para generar Xcode Previews para cuatro diferentes estados de entrada junto con Xcode Preview canvas renderizando la vista usando cada uno de esos estados

Para una vista pequeña que es fácil de construir, PreviewSnapshots proporciona alguna estructura adicional pero no hace mucho para reducir las líneas de código dentro de las vistas previas. El mayor beneficio para las vistas pequeñas viene cuando es el momento de escribir pruebas de instantáneas para la vista.

final class NameList_SnapshotTests: XCTestCase {
  func test_snapshot() {
    NameList_Previews.snapshots.assertSnapshots()
  }
}

Esa única aserción probará instantáneamente cada configuración en el PreviewSnapshots. La Figura 4 muestra el código de ejemplo junto a las imágenes de referencia en Xcode. Además, si se añaden nuevas configuraciones a las vistas previas, se comprobarán automáticamente sin necesidad de cambiar el código de prueba.

Figura 4: Prueba unitaria de Xcode utilizando PreviewSnapshots para probar cuatro estados de entrada diferentes definidos anteriormente con una única llamada a `assertSnapshots`.
Figura 4: Prueba unitaria de Xcode que utiliza PreviewSnapshots para probar cuatro estados de entrada diferentes definidos anteriormente con una única llamada a assertSnapshots

Para vistas más complejas con muchos argumentos, las ventajas son aún mayores.

Uso de PreviewSnapshots para una vista más compleja

En nuestro segundo ejemplo veremos un FormView que lleva varios Bindings , un mensaje de error opcional y un cierre de acción como argumentos de su inicializador. Esto demostrará el aumento de los beneficios de PreviewSnapshots a medida que aumenta la complejidad de la construcción de la vista.

struct FormView: View {
  init(
    firstName: Binding<String>,
    lastName: Binding<String>,
    email: Binding<String>,
    errorMessage: String?,
    submitTapped: @escaping () -> Void
  ) { ... }

  // ...
}

Desde PreviewSnapshots es genérico sobre el estado de entrada, podemos agrupar los distintos parámetros de entrada en una pequeña estructura de ayuda para pasarla al método configure bloque y sólo tienen que hacer el trabajo de construir un FormView una vez. Para mayor comodidad PreviewSnapshots proporciona una NamedPreviewState para simplificar la construcción de configuraciones de entrada agrupando el nombre de la vista previa junto con el estado de la vista previa.

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: "[email protected]"
        ),
        .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?
  }
}

En el código de ejemplo creamos un PreviewState que se ajuste a NamedPreviewState para guardar el nombre de la vista previa junto con el nombre, apellidos, dirección de correo electrónico y un mensaje de error opcional para construir la vista. A continuación, en el configure creamos una única instancia de FormView basándose en el estado de configuración introducido. Al devolver snapshots.preview de PreviewProvider.previewsPreviewSnapshots hará un bucle sobre los estados de entrada y construirá una vista previa Xcode con el nombre adecuado para cada estado, como se ve en la Figura 5. 

Figura 5: Editor Xcode mostrando código SwiftUI View usando PreviewSnapshots para generar Xcode Previews para tres diferentes estados de entrada junto con Xcode Preview canvas renderizando la vista usando cada uno de esos estados
Figura 5: Editor Xcode mostrando código SwiftUI View usando PreviewSnapshots para generar Xcode Previews para tres diferentes estados de entrada junto con Xcode Preview canvas renderizando la vista usando cada uno de esos estados

Una vez que hemos definido un conjunto de PreviewSnapshots para las vistas previas, podemos crear de nuevo un conjunto de pruebas de instantáneas con una única aserción de prueba unitaria.

final class FormView_SnapshotTests: XCTestCase {
  func test_snapshot() {
    FormView_Previews.snapshots.assertSnapshots()
  }
}

Al igual que en el ejemplo más sencillo anterior, este caso de prueba comparará cada uno de los estados de vista previa definidos en FormView_Previews.snapshots contra la imagen de referencia grabada en disco y generar un fallo de prueba si las imágenes no coinciden con lo esperado.

Conclusión

Este artículo ha discutido algunos de los beneficios del uso de Xcode Previews y SnapshotTesting cuando se desarrolla con SwiftUI. También ha demostrado algunos de los puntos de dolor y la duplicación de código que puede resultar de la utilización de esas dos tecnologías juntas y cómo PreviewSnapshots permite a los desarrolladores ahorrar tiempo mediante la reutilización del esfuerzo que ponen en la escritura de vistas previas de Xcode para las pruebas de instantáneas. 

Las instrucciones para incorporar PreviewSnapshots a tu proyecto, así como una aplicación de ejemplo que hace uso de PreviewSnapshots, están disponibles en GitHub.

About the Author

  • John Flanagan

    John Flanagan is a Software Engineer at DoorDash, since September 2021, on the iOS Infrastructure team focusing on technologies that enable iOS engineers at DoorDash to deliver features faster and more reliably.

Trabajos relacionados

Ubicación
Pune, India
Departamento
Ingeniería
Ubicación
Pune, India
Departamento
Ingeniería
Ubicación
Pune, India
Departamento
Ingeniería
Ubicación
San Francisco, CA; Sunnyvale, CA; Seattle, WA
Departamento
Ingeniería
Ubicación
San Francisco, CA
Departamento
Ingeniería