Articles

Flujo de Trabajo de Pruebas para Componentes Web

Cuando envías algo para ser usado por otros, asumes la responsabilidad de entregar un código seguro y estable. Una forma de abordar esto es probando tu código.

No importa lo pequeño que sea – no importa lo simple que sea tu proyecto, siempre debería haber pruebas.

Sí, sé que la realidad golpea duro y habrá muchos casos en los que eso no suceda – pero siempre debes esforzarte por tener pruebas

Descargo de responsabilidad

En este tutorial, vamos a hacer una versión simple de un elemento de entrada. Al final del mismo, obtendrás las habilidades y el conocimiento para poner en práctica las herramientas de prueba de open-wc; y construir un componente de entrada sólido, accesible y bien probado.

Advertencia

Este es un tutorial en profundidad que muestra algunas trampas y casos difíciles cuando se trabaja con componentes web. Esto es definitivamente para los usuarios más avanzados. Debes tener un conocimiento básico sobre LitElement y JSDoc Types. Tener una idea de lo que es Mocha, Chai BDD, Karma podría ayudar un poco también.

Estamos pensando en publicar una versión más fácil de digerir de esto, así que si eso es algo que le gustaría ver – háganoslo saber en los comentarios.

Si usted quiere jugar a lo largo – todo el código está en github.

¡Vamos a empezar!

Ejecuta en tu consola

$ npm init @open-wc# Results in this flow✔ What would you like to do today? › Scaffold a new project✔ What would you like to scaffold? › Web Component# Select with space! "Testing" => just enter will move one with no selection✔ What would you like to add? › Testing✔ Would you like to scaffold examples files for? › Testing✔ What is the tag name of your application/web component? … a11y-input✔ Do you want to write this file structure to disk? › YesWriting..... done✔ Do you want to install dependencies? › No

Para más detalles consulta https://open-wc.org/testing/.

Borra src/A11yInput.js

Modifica src/a11y-input.js a:

y test/a11y-input.test.js a:

Nuestras pruebas hasta ahora consisten en una sola función (la propiedad label) y una sola aserción (expect). Estamos utilizando la sintaxis BDD de karma y chai, por lo que agrupamos conjuntos de pruebas (it) bajo las características o APIs con las que se relacionan (describe).

Veamos si todo funciona correctamente ejecutando: npm run test.

Impresionante – tal como se esperaba (🥁), tenemos una prueba que falla 🙂

Cambiemos al modo de vigilancia, que ejecutará las pruebas continuamente cada vez que haga cambios en su código.

npm run test:watch

El siguiente código fue añadido en el vídeo anterior a src/a11y-input.js:

static get properties() { return { label: { type: String }, };}constructor() { super(); this.label = '';}

¿Hasta aquí todo bien? ¿Sigues con nosotros? Genial. Subamos un poco el juego…

Agregando una prueba para el Shadow DOM

Agreguemos una aserción para probar el contenido del shadow root de nuestro elemento.

Si queremos estar seguros de que nuestro elemento se comporta/parece igual debemos asegurarnos de que su estructura dom sigue siendo la misma.
Así que comparemos el shadow dom real con el que queremos que sea.

Como era de esperar, obtenemos:

Así que implementemos eso en nuestro elemento.

render() { return html` <slot name="label"></slot> <slot name="input"></slot> `;}

Interesante, la prueba debería ser verde… pero no lo es 🤔 Echemos un vistazo.

Tal vez te hayas fijado en esas extrañas etiquetas vacías de comentario <!---->. Son marcadores que lit-html utiliza para recordar dónde están las partes dinámicas, por lo que se puede actualizar de manera eficiente. Para las pruebas, sin embargo, esto puede ser un poco molesto de tratar.

Si usamos innerHTML para comparar el DOM, tendríamos que confiar en la simple igualdad de cadenas. En esas circunstancias, tendríamos que coincidir exactamente con los espacios en blanco del DOM generado, los comentarios, etc.; en otras palabras: tendrá que ser una coincidencia perfecta. Realmente todo lo que necesitamos probar es que los elementos que queremos renderizar se rendericen. Queremos probar el contenido semántico de la raíz de la sombra.

Así que vamos a probarlo 💪

// old:expect(el.shadowRoot.innerHTML).to.equal(`...`);// new:expect(el).shadowDom.to.equal(` <slot name="label"></slot> <slot name="input"></slot>`);

Bam 🎉

a11y input ✔ has by default an empty string as a label ✔ has a static shadowDom

¿Cómo funciona shadowDom.to.equal()?

  1. Obtiene el innerHTML de la raíz de la sombra
  2. La analiza (en realidad, el navegador lo analiza – no se necesita ninguna biblioteca)
  3. Lo normaliza (potencialmente cada etiqueta/propiedad en su propia línea)
  4. Percibe y normaliza la cadena HTML esperada
  5. Pasa ambas cadenas DOM normalizadas a la función de comparación por defecto de chai
  6. En caso de fallo, agrupa, y muestra cualquier diferencia de una manera clara

Si quieres saber más, por favor revisa la documentación de semantic-dom-diff.

Probando el DOM «ligero»

Podemos hacer exactamente lo mismo con el DOM ligero. (El DOM que nos proporcionará nuestro usuario o nuestros valores por defecto, es decir, el children del elemento).

Y vamos a implementarlo.

Así que probamos nuestro dom de luces y sombras 💪 y nuestras pruebas se ejecutan limpias 🎉

Nota: El uso de la API del DOM en el ciclo de vida de un elemento de luz es un anti-patrón, sin embargo para permitir a11y podría ser un caso de uso real – de todos modos es grande para fines de ilustración

El uso de nuestro elemento en una App

Así que ahora que tenemos una entrada básica de a11y vamos a usarlo – y probarlo – en nuestra aplicación.

De nuevo empezamos con un esqueleto src/my-app.js

Y nuestro test en test/my-app.test.js;

Ejecutar el test => falla y entonces añadimos la implementación a src/a11y-input.js

render() { return html` <h1>My Filter App</h1> <a11y-input></a11y-input> `;}

Pero ¡oh no! Eso debería ser verde ahora…

¿Qué está pasando?
¿Recuerdas que teníamos un test específico para asegurar el light-dom de a11y-input?
Así que incluso si el usuario sólo pone <a11y-input></a11y-input> en su código – lo que realmente sale es

<a11y-input> <label slot="label"></label> <input slot="input"></a11y-input>

por ejemplo, a11y-input está realmente creando nodos dentro de tu my-app shadow dom. Es absurdo. Para nuestro ejemplo aquí decimos que eso es lo que queremos.
Entonces, ¿cómo podemos seguir probándolo?

Por suerte .shadowDom tiene otro as en la manga; nos permite ignorar partes del dom.

expect(el).shadowDom.to.equal(` <h1>My Filter App</h1> <a11y-input></a11y-input>`, { ignoreChildren: });

Incluso podemos especificar las siguientes propiedades también:

  • ignoreChildren
  • ignoreTags
  • ignoreAttributes (globalmente o para etiquetas específicas)

Para más detalles por favor vea semantic-dom-diff.

Pruebas de instantáneas

Si tienes muchos árboles de doms grandes escribir/mantener todas esas esperas escritas manualmente va a ser duro.
Para ayudarte con eso hay instantáneas semiautomáticas.

Así que si cambiamos nuestro código

// fromexpect(el).shadowDom.to.equal(` <slot name="label"></slot> <slot name="input"></slot>`);// toexpect(el).shadowDom.to.equalSnapshot();

Si ahora ejecutamos npm run test se creará un fichero __snapshots__/a11y input.md y se llenará con algo así

# `a11y input`#### `has a static shadowDom```html<slot name="label"></slot><slot name="input"></slot>``

Lo que escribimos antes a mano puede ser ahora autogenerado en el init o de forma forzada mediante npm run test:update-snapshots.

Si el archivo __snapshots__/a11y input.md ya existe lo comparará con la salida y obtendrá errores si su salida html cambió.

Para más detalles, por favor vea semantic-dom-diff.

Creo que ya es suficiente sobre la comparación de árboles dom…
Es hora de un cambio 🤗

Cobertura de código

Otra métrica útil que obtenemos al probar con la configuración de open-wc es la cobertura de código.
Entonces, ¿qué significa y cómo podemos obtenerla? La cobertura de código es una medida de cuánto de nuestro código es comprobado por las pruebas. Si hay una línea, declaración, función o rama (por ejemplo, la declaración if/else) que nuestras pruebas no cubren nuestra puntuación de cobertura se verá afectada.
Un simple npm run test es todo lo que necesitamos y obtendremos lo siguiente:

=============================== Coverage summary ===============================Statements : 100% ( 15/15 )Branches : 100% ( 0/0 )Functions : 100% ( 5/5 )Lines : 100% ( 15/15 )================================================================================

Lo que significa que el 100% de las declaraciones, ramas, funciones y líneas de nuestro código están cubiertas por las pruebas. Así que vayamos en la dirección contraria y añadamos código a src/a11y-input.js antes de añadir una prueba. Digamos que queremos acceder al valor de nuestro input directamente a través de nuestro elemento personalizado y siempre que su valor sea ‘cat’ queremos registrar algo.

El resultado es muy diferente

Nuestra cobertura es mucho menor que antes. Nuestro comando de prueba incluso falla, aunque todas las pruebas se ejecutan con éxito.
Esto se debe a que por defecto la configuración de open-wc establece un umbral del 90% para la cobertura del código.

Si queremos mejorar la cobertura necesitamos añadir pruebas – así que hagámoslo

uh oh 😱 queríamos mejorar la cobertura pero ahora necesitamos arreglar un bug real primero 😞

Eso fue inesperado… a primera vista, no sé realmente lo que significa… mejor comprobar algunos nodos reales e inspeccionarlos en el navegador.

Depuración en el navegador

Cuando ejecutamos nuestra prueba con watch, karma configura un entorno de navegador persistente para ejecutar las pruebas.

  • Asegúrate de que has empezado con npm run test:watch
  • visita http://localhost:9876/debug.html

Deberías ver algo así

Puedes hacer clic en el botón de reproducción con un círculo para ejecutar sólo una prueba individual.

Así que vamos a abrir el Chrome Dev Tools (F12) y poner un depurador en el código de la prueba.

Dang.. el error ocurre incluso antes de ese punto…
Errores «fatales» como este son un poco más difíciles ya que no son pruebas que fallan sino una especie de fusión completa de su componente completo.

Ok, vamos a poner un poco de código en el setter directamente.

set value(newValue) { debugger;

Muy bien, eso ha funcionado así que en nuestra consola de chrome escribimos console.log(this)veamos lo que tenemos aquí

<a11y-input> #shadow-root (open)</a11y-input>

Ahí lo tenemos – el dom de la sombra aún no está renderizado cuando se llama al setter.
Así que vamos a estar seguros y añadimos una comprobación antes

set value(newValue) { if (newValue === 'cat') { console.log('We like cats too :)'); } if (this.inputEl) { this.inputEl.value = newValue; }}

El error de Feld se ha ido 🎉
Pero ahora tenemos una prueba que falla 😭

✖ can set/get the input value directly via the custom elementAssertionError: expected '' to equal 'foo'

Puede que necesitemos un cambio de táctica 🤔
Podemos añadirla como una propiedad value separada y sincronizarla cuando sea necesario.

¡Y por fin volvemos a la carga! 🎉

ok bug arreglado – ¿podemos volver a la cobertura? gracias 🙏

Volver a la cobertura

Con esta prueba añadida hemos hecho algunos progresos.

Sin embargo todavía no estamos del todo – la pregunta es ¿por qué?

Para averiguarlo abre coverage/index.html en tu navegador. No es necesario un servidor web, simplemente abre el archivo en tu navegador – en un mac puedes hacerlo desde la línea de comandos con open coverage/index.html

Verás algo como esto

Una vez que hagas clic en a11y-input.js obtendrás una información línea por línea de la frecuencia con la que se ejecutaron.
Así que podemos ver inmediatamente qué líneas no son ejecutadas todavía por nuestras pruebas.

Así que vamos a añadir un test para eso

=============================== Coverage summary ===============================Statements : 100% ( 24/24 )Branches : 75% ( 3/4 )Functions : 100% ( 7/7 )Lines : 100% ( 24/24 )================================================================================

Con esto, volvemos a tener el 100% de las sentencias pero todavía nos falta algo en las ramas.
¿Vamos a ver por qué?

Este E significa else path not taken.
Así que cada vez que se llama a la función update siempre hay una propiedad value en las changedProperties.

También tenemos label así que es buena idea probarlo. 👍

boom 100% 💪 ganamos 🥇

=============================== Coverage summary ===============================Statements : 100% ( 24/24 )Branches : 100% ( 4/4 )Functions : 100% ( 7/7 )Lines : 100% ( 24/24 )================================================================================

Pero espera que ni siquiera terminamos la prueba anterior – el código sigue

 // somehow check that console.log was called

¿Cómo es que tenemos un 100% de cobertura de pruebas?

Primero tratemos de entender cómo funciona la cobertura de código 🤔
La forma de medir la cobertura de código es aplicando una forma de instrumentation. En resumen, antes de que nuestro código se ejecute se modifica (instrumented) y se comporta algo así:

Nota: Esta es una versión súper simplificada con fines ilustrativos.

Básicamente, tu código se llena de muchas muchas banderas. Basado en qué banderas se disparan se crea una estadística.

Así que el 100% de cobertura de pruebas sólo significa que cada línea que tienes en tu código se ejecutó al menos una vez después de que todas tus pruebas terminaron. No significa que hayas probado todo, o si tus pruebas hacen las aserciones correctas.

Así que aunque ya tengamos un 100% de cobertura de código vamos a mejorar nuestra prueba de registro.

Deberías, por tanto, ver la cobertura de código como una herramienta que sólo te orienta y ayuda a detectar algunas pruebas que faltan, más que como una garantía dura de la calidad del código.

Espiar el código

Si quieres comprobar con qué frecuencia o con qué parámetros se llama a una función, eso se llama espiar.
open-wc recomienda el venerable paquete sinon, que proporciona muchas herramientas para espiar y otras tareas relacionadas.

npm i -D sinon

Así que creas un espía en un objeto específico y luego puedes comprobar la frecuencia con la que se llama.

Uh oh… la prueba falla:

AssertionError: expected 0 to equal 1

Mezclar con objetos globales como console podría tener efectos secundarios así que mejor refactoricemos usando una función de registro dedicada.

Esto resulta en que no hay objeto global en nuestro código de prueba – dulce 🤗

Sin embargo, todavía obtenemos el mismo error. Vamos a depurar… boohoo aparentemente update no está sincronizado – una suposición errónea que hice 🙈 Estoy diciendo que las suposiciones son peligrosas con bastante frecuencia – todavía caigo en ella de vez en cuando 😢.

Entonces, ¿qué podemos hacer? Tristemente parece que no hay una api pública para hacer algunas acciones de sincronización desencadenadas por una actualización de propiedades.
Creemos un issue para ello https://github.com/Polymer/lit-element/issues/643.

Por ahora aparentemente, la única manera es confiar en una api privada. 🙈
Además, necesitábamos mover la sincronización de valores a updated para que se ejecute después de cada renderizado del dom.

Y aquí está el test actualizado para el logging

Wow, ha sido un poco más difícil de lo esperado pero lo hemos conseguido 💪

SUMMARY:✔ 7 tests completedTOTAL: 7 SUCCESS

Ejecución de pruebas sin Karma Framework

El Karma Framework es potente y rico en funcionalidades, pero a veces podemos querer reducir nuestro régimen de pruebas. Lo bueno de todo lo que hemos propuesto hasta ahora es que sólo utilizamos módulos es estándar del navegador sin necesidad de transpilación, con la única excepción de los especificadores de módulos desnudos.
Así que sólo con crear un test/index.html.

y abriéndolo a través de owc-dev-server en chrome, funcionará perfectamente.
Tenemos todo funcionando sin webpack o karma – dulce 🤗

Haz la cosa de los navegadores

Ahora nos sentimos bastante cómodos con nuestro componente web. Está probado y cubierto; sólo hay un paso más – queremos asegurarnos de que se ejecuta y se prueba en todos los navegadores.

Así que vamos a ejecutarlo

npm run test:bsSUMMARY:✔ 42 tests completedTOTAL: 42 SUCCESS

¡Sí, eso funciona bien! 🤗

Si hay pruebas que fallan las mostrará en el resumen con el navegador específico donde falló.

Si necesitas depurar un navegador en particular:

También si quieres ajustar el navegador que se prueba puedes ajustar tu karma.bs.config.js.

Por ejemplo, si quieres añadir el Firefox ESR a tu lista.

O quizás quieras probar sólo 2 navegadores específicos?

Nota: Esto utiliza las estrategias de fusión de webpack reemplazar.

Recapitulación rápida

  • Las pruebas son importantes para cada proyecto. Asegúrate de escribir todas las que puedas.
  • Intenta mantener tu cobertura de código alta, pero recuerda que no es una garantía mágica, así que no siempre tiene que ser del 100%.
  • Debuja en el navegador a través de npm run test:watch. Para los navegadores heredados, utilice npm run test:legacy.watch.

¿Qué sigue?

  • Ejecute las pruebas en su CI (funciona perfectamente junto con browserstack). Ver nuestras recomendaciones en la automatización.

Síguenos en Twitter, o sígueme en mi Twitter personal.
Asegúrate de consultar nuestras otras herramientas y recomendaciones en open-wc.org.

Gracias a Pascal y Benny por sus comentarios y por ayudarme a convertir mis garabatos en una historia que se pueda seguir.