Picasso: Cómo probar una biblioteca de componentes

Publicado: 2022-03-11

Recientemente se lanzó una nueva versión del sistema de diseño de Toptal que requería que hiciéramos cambios en casi todos los componentes de Picasso, nuestra biblioteca de componentes interna. Nuestro equipo se enfrentó a un desafío: ¿Cómo nos aseguramos de que no ocurran regresiones?

La respuesta corta es, como era de esperar, pruebas. Muchas pruebas.

No revisaremos los aspectos teóricos de las pruebas ni discutiremos los diferentes tipos de pruebas, su utilidad ni explicaremos por qué debería probar su código en primer lugar. Nuestro blog y otros ya han cubierto esos temas. En su lugar, nos centraremos únicamente en los aspectos prácticos de las pruebas.

Siga leyendo para saber cómo los desarrolladores de Toptal escriben las pruebas. Nuestro repositorio es público, por lo que usamos ejemplos del mundo real. No hay abstracciones ni simplificaciones.

Pirámide de prueba

No tenemos definida una pirámide de prueba per se, pero si la tuviéramos se vería así:

Ilustración de la pirámide de prueba

La pirámide de pruebas de Toptal ilustra las pruebas que enfatizamos.

Pruebas unitarias

Las pruebas unitarias son sencillas de escribir y fáciles de ejecutar. Si tiene muy poco tiempo para escribir pruebas, deberían ser su primera opción.

Sin embargo, no son perfectos. Independientemente de la biblioteca de prueba que elija (Jest and React Testing Library [RTL] en nuestro caso), no tendrá un DOM real y no le permitirá verificar la funcionalidad en diferentes navegadores, pero le permitirá eliminar elimine la complejidad y pruebe los componentes básicos simples de su biblioteca.

Las pruebas unitarias no solo agregan valor al probar el comportamiento del código, sino también al verificar la capacidad de prueba general del código. Si no puede escribir pruebas unitarias fácilmente, es probable que tenga un código incorrecto.

Pruebas de regresión visual

Incluso si tiene una cobertura de prueba unitaria del 100 %, eso no significa que los componentes se vean bien en todos los dispositivos y navegadores.

Las regresiones visuales son particularmente difíciles de detectar con las pruebas manuales. Por ejemplo, si la etiqueta de un botón se mueve 1px, ¿lo notará un ingeniero de control de calidad? Afortunadamente, hay muchas soluciones a este problema de visibilidad limitada. Puede optar por soluciones integrales de nivel empresarial como LambdaTest o Mabl. Puede incorporar complementos, como Percy, en sus pruebas existentes, así como soluciones de bricolaje como Loki o Storybook (que es lo que usábamos antes de Picasso). Todos tienen inconvenientes: algunos son demasiado costosos, mientras que otros tienen una curva de aprendizaje pronunciada o requieren demasiado mantenimiento.

¡Happo al rescate! Es un competidor directo de Percy, pero es mucho más barato, admite más navegadores y es más fácil de usar. ¿Otro gran punto de venta? Es compatible con la integración de Cypress, lo cual fue importante porque queríamos dejar de usar Storybook para las pruebas visuales. Nos encontramos en situaciones en las que teníamos que crear historias solo para poder garantizar la cobertura de la prueba visual, no porque necesitáramos documentar ese caso de uso. Eso contaminó nuestros documentos y los hizo más difíciles de entender. Queríamos aislar las pruebas visuales de la documentación visual.

Pruebas de integración

Incluso si dos componentes tienen pruebas unitarias y visuales, eso no garantiza que funcionen juntos. Por ejemplo, encontramos un error en el que una información sobre herramientas no se abre cuando se usa en un elemento desplegable, pero funciona bien cuando se usa sola.

Para garantizar que los componentes se integren bien, utilizamos la función de prueba de componentes experimentales de Cypress. Al principio, no estábamos satisfechos con el bajo rendimiento, pero pudimos mejorarlo con una configuración de paquete web personalizado. ¿El resultado? Pudimos usar la excelente API de Cypress para escribir pruebas de rendimiento que aseguran que nuestros componentes funcionen bien juntos.

Aplicación de la pirámide de prueba

¿Cómo se ve todo esto en la vida real? ¡Probemos el componente Accordion!

Su primer instinto podría ser abrir su editor y comenzar a escribir código. ¿Mi consejo? Dedique algo de tiempo a comprender todas las características del componente y escriba qué casos de prueba desea cubrir.

GIF de demostración de la biblioteca de componentes de Picasso

¿Qué probar?

Aquí hay un desglose de los casos que nuestras pruebas deberían cubrir:

  • Estados : los acordeones se pueden expandir y contraer, su estado predeterminado se puede configurar y esta función se puede deshabilitar
  • Estilos : los acordeones pueden tener variaciones de borde
  • Contenido – Pueden integrarse con otras unidades de la biblioteca
  • Personalización : el componente puede tener sus estilos anulados y puede tener iconos de expansión personalizados
  • Devoluciones de llamada: cada vez que cambia el estado, se puede invocar una devolución de llamada

GIF de demostración de la biblioteca de componentes de Picasso - componente de acordeón

¿Cómo probar?

Ahora que sabemos lo que tenemos que probar, consideremos cómo hacerlo. Tenemos tres opciones de nuestra pirámide de pruebas. Queremos lograr la máxima cobertura con la mínima superposición entre las secciones de la pirámide. ¿Cuál es la mejor manera de probar cada caso de prueba?

  • Estados : las pruebas unitarias pueden ayudarnos a evaluar si los estados cambian en consecuencia, pero también necesitamos pruebas visuales para asegurarnos de que el componente se represente correctamente en cada estado.
  • Estilos : las pruebas visuales deberían ser suficientes para detectar regresiones de las diferentes variantes.
  • Contenido : una combinación de pruebas visuales y de integración es la mejor opción, ya que Accordions se puede usar en combinación con muchos otros componentes.
  • Personalización : podemos usar una prueba unitaria para verificar si un nombre de clase se aplica correctamente, pero necesitamos una prueba visual para asegurarnos de que el componente y los estilos personalizados funcionen en conjunto.
  • Devoluciones de llamada: las pruebas unitarias son ideales para garantizar que se invoquen las devoluciones de llamada correctas

La pirámide de prueba del acordeón

Pruebas unitarias

El conjunto completo de pruebas unitarias se puede encontrar aquí. Hemos cubierto todos los cambios de estado, la personalización y las devoluciones de llamada:

 it('toggles', async () => { const handleChange = jest.fn() const { getByText, getByTestId } = renderAccordion({ onChange: handleChange, expandIcon: <span data-test /> }) fireEvent.click(getByTestId('accordion-summary')) await waitFor(() => expect(getByText(DETAILS_TEXT)).toBeVisible()) fireEvent.click(getByTestId('trigger')) await waitFor(() => expect(getByText(DETAILS_TEXT)).not.toBeVisible()) fireEvent.click(getByText(SUMMARY_TEXT)) await waitFor(() => expect(getByText(DETAILS_TEXT)).toBeVisible()) expect(handleChange).toHaveBeenCalledTimes(3) })

Pruebas de regresión visual

Las pruebas visuales se encuentran en este bloque de descripción de Cypress. Las capturas de pantalla se pueden encontrar en el panel de control de Happo.

Puede ver todos los diferentes estados de componentes, variantes y personalizaciones que se han registrado. Cada vez que se abre un PR, CI compara las capturas de pantalla que Happo ha almacenado con las tomadas en su sucursal:

 it('renders', () => { mount( <TestingPicasso> <TestAccordion /> </TestingPicasso> ) cy.get('body').happoScreenshot() }) it('renders disabled', () => { mount( <TestingPicasso> <TestAccordion disabled /> <TestAccordion expandIcon={<Check16 />} /> </TestingPicasso> ) cy.get('body').happoScreenshot() }) it('renders border variants', () => { mount( <TestingPicasso> <TestAccordion borders='none' /> <TestAccordion borders='middle' /> <TestAccordion borders='all' /> </TestingPicasso> ) cy.get('body').happoScreenshot() })

Pruebas de integración

Escribimos una prueba de "ruta incorrecta" en este bloque de descripción de Cypress que afirma que Accordion aún funciona correctamente y que los usuarios pueden interactuar con el componente personalizado. También agregamos afirmaciones visuales para mayor confianza:

 describe('Accordion with custom summary', () => { it('closes and opens', () => { mount(<AccordionCustomSummary />) toggleAccordion() getAccordionContent().should('not.be.visible') cy.get('[data-testid=accordion-custom-summary]').happoScreenshot() toggleAccordion() getAccordionContent().should('be.visible') cy.get('[data-testid=accordion-custom-summary]').happoScreenshot() }) // … })

Integración continua

Picasso se basa casi por completo en GitHub Actions para el control de calidad. Además, agregamos ganchos Git para verificaciones de calidad del código de archivos preparados. Recientemente migramos de Jenkins a GHA, por lo que nuestra configuración aún se encuentra en la etapa MVP.

El flujo de trabajo se ejecuta en cada cambio en la sucursal remota en orden secuencial, siendo la integración y las pruebas visuales la última etapa porque son las más costosas de ejecutar (tanto en términos de rendimiento como de costo monetario). A menos que todas las pruebas se completen correctamente, la solicitud de extracción no se puede fusionar.

Estas son las etapas por las que pasa GitHub Actions cada vez:

  1. Instalación de dependencia
  2. Control de versiones : valida que el formato de las confirmaciones y el título de relaciones públicas coincidan con las confirmaciones convencionales
  3. Lint : ESlint garantiza un código de buena calidad
  4. Compilación de TypeScript : verifique que no haya errores de tipo
  5. Compilación de paquetes : si los paquetes no se pueden compilar, no se publicarán correctamente; nuestras pruebas de Cypress también esperan código compilado
  6. Pruebas unitarias
  7. Integración y pruebas visuales

El flujo de trabajo completo se puede encontrar aquí. Actualmente, lleva menos de 12 minutos completar todas las etapas.

Testabilidad

Como la mayoría de las bibliotecas de componentes, Picasso tiene un componente raíz que debe envolver todos los demás componentes y puede usarse para establecer reglas globales. Esto hace que sea más difícil escribir pruebas por dos razones: inconsistencias en los resultados de las pruebas, según los accesorios utilizados en el envoltorio; y repetitivo adicional:

 import { render } from '@testing-library/react' describe('Form', () => { it('renders', () => { const { container } = render( <Picasso loadFavicon={false} environment='test'> <Form /> </Picasso> ) expect(container).toMatchSnapshot() }) })

Resolvimos el primer problema creando un TestingPicasso que condiciona las reglas globales para la prueba. Pero es molesto tener que declararlo para cada caso de prueba. Es por eso que creamos una función de representación personalizada que envuelve el componente pasado en un TestingPicasso y devuelve todo lo disponible desde la función de representación de RTL.

Nuestras pruebas ahora son más fáciles de leer y fáciles de escribir:

 import { render } from '@toptal/picasso/test-utils' describe('Form', () => { it('renders', () => { const { container } = render(<Form />) expect(container).toMatchSnapshot() }) })

Conclusión

La configuración que se describe aquí está lejos de ser perfecta, pero es un buen punto de partida para aquellos que son lo suficientemente aventureros como para crear una biblioteca de componentes. He leído mucho sobre probar pirámides, pero no siempre es fácil aplicarlas en la práctica. Por lo tanto, los invito a explorar nuestra base de código y aprender de nuestros errores y aciertos.

Las bibliotecas de componentes son únicas porque atienden a dos tipos de audiencias: los usuarios finales que interactúan con la interfaz de usuario y los desarrolladores que usan su código para crear sus propias aplicaciones. Invertir tiempo en un marco de prueba sólido beneficiará a todos. Invertir tiempo en mejoras en la capacidad de prueba lo beneficiará a usted como mantenedor y a los ingenieros que usan (y prueban) su biblioteca.

No discutimos cosas como la cobertura del código, las pruebas de extremo a extremo y las políticas de versión y lanzamiento. El breve consejo sobre esos temas es: publique a menudo, practique el control de versiones semántico adecuado, tenga transparencia en sus procesos y establezca expectativas para los ingenieros que confían en su biblioteca. Es posible que revisemos estos temas con más detalle en publicaciones posteriores.