Video Summary and Transcription
Esta charla trata sobre la primera contribución de un desarrollador al Testing Library de código abierto, explorando cómo funciona y su importancia en las pruebas. Se discuten los desafíos de las pruebas en un entorno Node y el uso de la consulta getByRole para encontrar elementos. La charla también destaca las complejidades de los roles implícitos y la necesidad de atributos específicos para filtrar elementos. Se enfatiza la importancia de verificar la visibilidad y accesibilidad al consultar elementos y el proceso de limpieza de las pruebas.
1. Introducción a Testing Library
¡Hola a todos! Hoy quiero compartir una breve historia sobre mi primera contribución de código abierto a Testing Library. Encontré un problema interesante relacionado con el proceso de compilación y React 17, lo que me llevó a depurar y hacer una contribución significativa. Fue un emocionante viaje al desarrollo de código abierto.
¡Hola a todos y muchas gracias por unirse a mí de forma remota hoy para entender cómo funciona la biblioteca de pruebas Testing Library bajo el capó.
Antes de presentarme, quiero comenzar con una breve historia. Érase una vez, hace tres años, estaba buscando hacer mi primera contribución de código abierto. Decidí elegir una herramienta que uso a diario, así que elegí Testing Library.
Mientras navegaba por los problemas, encontré uno interesante. El problema era un error en nuestro proceso de integración continua (CI) donde la compilación fallaba para la próxima versión de React. En ese momento, era React 17. Decidí que este era un tema interesante. Decidí investigarlo. Depuré tanto Testing Library como React para tratar de entender qué estaba sucediendo allí y por qué nuestra compilación fallaba.
Unos días después, tenía una solicitud de extracción (pull request). Fue revisada y fusionada, y esa fue mi primera contribución significativa a Testing Library. Recuerdo ese día con bastante claridad. Eran alrededor de las 8 pm. Estaba extremadamente feliz. Decidí irme a dormir. Todavía estaba lleno de adrenalina. No dormí mucho, pero después de una buena noche de sueño, me desperté feliz y alegre por hacer mi primera contribución significativa. Hice mi café. Me senté en mi escritorio. Decidí abrir GitHub y ver qué había sucedido durante la noche.
Lo primero que vi y que hizo que mi estado de ánimo cambiara de inmediato fue este problema. La versión 10.4.4 está causando un problema de tiempo de espera con useEffect y solo usa temporizadores falsos. Luego, seguí desplazándome un poco más. También vi este. Error de tipo, no se puede leer la propiedad, versión de indefinido y también este. La función de programación está llamando a la prioridad normal como una función en lugar de una devolución de llamada. Y eso no es una ilustración. Todos estos problemas estaban abiertos durante la noche mientras yo dormía. Y sí, qué inmersión en el código abierto.
2. Understanding How Testing Library Works
En esta parte, compartiré una historia sobre mi contribución de código abierto. Luego me presentaré y explicaré cómo funciona la biblioteca de pruebas. Nos alinearemos en los conceptos básicos y discutiremos la importancia de la confianza en las pruebas. ¡Sumergámonos!
Todos ellos están directamente relacionados con mi cambio. Incluso contienen fragmentos de código que leí. Y esta es una ilustración de mí en ese momento específico. Pero en lugar de darle algo a Stephen Hawking, se lo di a Ken C. Dodds. Y, wow, me sentí avergonzado. Afortunadamente, esto sucedió en el momento en que la biblioteca de pruebas de React tenía solo 1.8 millones de descargas semanales. Así que estuvo bien. En este momento, tenemos 8.7 millones de descargas semanales. Eso es bastante.
En esta charla voy a intentar darte algo de contexto sobre cómo funciona la biblioteca de pruebas para que no cometas los mismos errores que cometí en mi primera contribución. Y no te olvides de esta historia. Volveremos a ella al final.
Después de esta breve historia, hagámoslo oficial. Hola a todos. Mi nombre es Mattan Boronkraut. Soy un ingeniero de software senior en Microsoft. Y hoy estoy aquí con mi sombrero de mantenedor de la biblioteca de pruebas para explicarte cómo funciona la biblioteca de pruebas bajo el capó. Si quieres seguirme, este es el sitio donde escribo algunos artículos. Básicamente, escribo por diversión. Tenía analíticas y vi que tengo alrededor de cinco visitas a la semana, así que decidí eliminar las analíticas. No quiero estos datos en mi vida. Y este es mi nombre de usuario en Twitter o x-handle.
Antes de sumergirnos, alineémonos en los conceptos básicos. Entonces, ¿qué es la biblioteca de pruebas? La biblioteca de pruebas es un conjunto de utilidades de prueba simples y completas que fomentan buenas prácticas de prueba. Y nuestra API tiene como objetivo responder a este principio rector: cuanto más se parezca tu prueba a la forma en que se usa tu software, más confianza pueden darte. Y todos podemos estar de acuerdo en que la confianza es lo que buscamos cuando escribimos nuestras pruebas. Queremos tener confianza antes de enviar a producción. Y hoy entenderemos cómo estamos logrando exactamente eso, cómo estamos creando la confianza una API que parece simple en la superficie, pero que en realidad es bastante compleja. Algunas estadísticas más, la biblioteca de pruebas de DOM, que es nuestro paquete principal.
3. Using and Testing React Testing Library
Exploraremos cómo se utiliza y se admite la biblioteca de pruebas de React en varios frameworks. Luego, nos sumergiremos en un ejemplo sencillo de un componente de React y su prueba correspondiente. Entendamos los detalles de implementación.
Se utiliza en todos nuestros paquetes, con alrededor de 12.7 millones de descargas semanales. Y como ya hemos visto, la biblioteca de pruebas de React tiene alrededor de 8.7 millones de descargas semanales. Admitimos más de 10 frameworks utilizando algo que llamamos un envoltorio de framework. Esto significa que tenemos envoltorios de framework para Angular, Vue, Svelte y muchos más. Y hay más que fueron escritos por la comunidad.
Para comprender cómo hemos construido este paquete popular, tomemos un ejemplo utilizando React y la biblioteca de pruebas de React. Este es un componente de React bastante simple. Es un componente de contador que cuenta la cantidad de tazas de café que he tomado hoy. Y para ser honesto, solo he tomado una hoy, pero este es el componente. Podemos ver que tiene un botón para aumentar el contador. Tiene un botón para disminuir el contador. Y también tiene un control deslizante con un valor mínimo de 0 y un valor máximo de 6. Y 6 tazas de café al día es bastante, así que espero que ninguno de ustedes esté tomando más de 6 tazas de café al día. Veamos el JSX por un momento para explicar cómo se construye este componente. Tiene un useState con un valor inicial de 0. Tiene un controlador de eventos onClick que incrementa o disminuye el contador. Y en nuestro JSX podemos ver que tenemos un input de tipo rango, que es nuestro control deslizante, con un valor mínimo de 0 y un valor máximo de 6. Tenemos un botón con el signo menos, tenemos el botón con el signo más, y también tenemos un elemento de salida que muestra el contador actual. Entonces, ¿cómo probamos este componente? Esta es una prueba sencilla que tiene como objetivo verificar que cada vez que hacemos clic en el botón de suma, el contador se incrementa y el usuario puede ver que se ha incrementado. En la primera línea, lo que estamos haciendo es renderizar el componente en un entorno determinado. Y hablaremos de eso en un momento. Justo después de eso, estamos intentando buscar el elemento con el rol de botón y el nombre de suma. Después de tener ese botón en nuestras manos, estamos disparando un evento llamado click, porque estamos tratando de simular un evento de clic. Justo después de eso, estamos verificando que el contenido que el usuario ve en ese momento sea 1 en lugar de 0. Intentemos comprender qué significa cada línea de esta prueba sumergiéndonos en la implementación real. Así que la función de renderizado. Estamos diciendo renderizar, pero ¿dónde renderizamos? Cuando escribimos nuestras pruebas, el entorno predeterminado para nuestros ejecutores de pruebas suele ser Node. Y cuando digo ejecutores de pruebas, puede ser VTest o simplemente el que elijas. Y desafortunadamente, Node no tiene un objeto window.
4. Node Environment and Test Rendering
Node solo tiene un objeto global y no admite todas las funcionalidades requeridas por nuestros componentes. Para solucionar esto, utilizamos TestEnvironment.js DOM, que crea un objeto window para las pruebas. Sin embargo, js-dom y happy-dom, las implementaciones de JavaScript del DOM, tienen limitaciones y son más lentas que los navegadores reales. A pesar de estos desafíos, tenemos implementaciones específicas para renderizar y limpiar componentes en la biblioteca de pruebas de React.
Node solo tiene un objeto global. Y como probablemente sabes, el objeto global no es el objeto window. Y nuestros componentes utilizan, y esto es solo un ejemplo, CreateElement, QuerySelector, getElementById, OnClick, IntersectionObserver, InnerHeight, Location, LocalStorage, Navigator, History y muchos más. Así que esto es solo un ejemplo. Y ninguno de estos es compatible dentro de Node, por lo que necesitamos alguna forma de implementarlos o usarlos. Y eso es un problema.
Entonces, nuestros componentes utilizan funcionalidades de window. Esto significa que necesitamos un objeto. Permíteme ayudarte con esto. Te presento TestEnvironment.js DOM. Creo que la mayoría de ustedes tiene esta línea en su archivo Just Config. Lo que hace esta línea es crear un objeto window para que todos lo usen. Esto significa que una vez que tengamos esta línea en nuestro archivo Just Config, podremos usar window en nuestras pruebas. Pero, ¿qué es js-dom? Js-dom o happy-dom, que son básicamente competidores, son implementaciones de JavaScript del DOM. Desafortunadamente, no contienen todas las funcionalidades. Si alguna vez intentaste hacer clic en un enlace dentro de una prueba, probablemente hayas visto que la ubicación no cambia. Eso se debe a que no está implementado en js-dom. Por lo tanto, no hay navegación y tampoco hay diseño, lo que significa que todos los componentes se encuentran uno encima del otro y no hay un diseño real dentro de ese entorno específico.
Otro problema, tal vez el problema más grave, es que js-dom y happy-dom son más lentos que los navegadores. Y la razón de esto es que ambos son implementaciones de JavaScript del DOM, y los navegadores reales generalmente están escritos con un lenguaje de nivel inferior, por lo que probablemente sean mucho más rápidos. Solo como ejemplo, una consulta de selección de todos los elementos dentro de js-dom tarda aproximadamente cuatro milisegundos en completarse, y eso es bastante cuando hablamos de pruebas. Debería ser lo más rápido posible. Ahora que hemos entendido dónde estamos renderizando, veamos el código para la renderización específica en la biblioteca de pruebas de React. Este es el logotipo de GOAT, y este es el logotipo de la biblioteca de pruebas de React. Esto significa que el código que estamos viendo ahora es específico de la biblioteca de pruebas de React, y cada framework que tenemos tiene una implementación específica para estas funcionalidades específicas. Entonces, lo que tenemos aquí es un conjunto de código. Significa que eliminé algunas cosas que no eran relevantes para esta diapositiva porque solo nos confundirían. En la primera línea, lo que estamos haciendo es crear una ruta de React. La ruta de React es lo que probablemente también estés creando en index.js o index.js porque debemos tener una ruta de React para renderizar componentes de React. Justo después de tener esa ruta de React, estamos empujando referencias al contenedor y a la ruta para poder limpiarlo más adelante.
5. Rendering and Querying with getByRoll
Utilizamos la función render para renderizar la interfaz de usuario y obtener un valor de retorno que incluye un contenedor, una función de desmontaje, una función de volver a renderizar y la consulta de la biblioteca de pruebas. La consulta getByRoll es compleja y se basa en el árbol de accesibilidad. Nos permite encontrar elementos con roles específicos, que están definidos por el Consorcio World Wide Web. Al transformar la consulta getByRoll, utilizamos document querySelectorAll con el rol especificado.
Y podemos ver que también estamos llamando a route.render, que es básicamente la función de renderizado. Así que le estamos diciendo a React: por favor, renderiza esta UI específica. Podemos ver que está envuelto en Act. Act es una utilidad de testing escrita por React, y no la vamos a cubrir en esta charla. En nuestro valor de retorno, lo que tenemos es un contenedor, una función de desmontaje, una función de volver a renderizar, y la consulta completa de la biblioteca de pruebas está vinculada a ese contenedor específico, vinculada a ese elemento específico que acabas de renderizar. Así que esto es bastante simple. Creo que la mayoría de ustedes probablemente pueden tomar este ejemplo y crear una función de renderizado para cada framework que estén utilizando si aún no está disponible. Así que esta fue la parte de renderizado.
La parte de getByRoll es mucho más compleja, así que vamos a sumergirnos en ella. Este es el logotipo de la biblioteca de pruebas del DOM. Esto significa que este code no es específico de un framework en particular. Mientras tengamos un DOM, este code puede ejecutarse y funcionará. Estamos intentando consultar un elemento con el rol de botón y el nombre plus. Y si recuerdas nuestra UI, así es como se ve. Tenemos un deslizador, y tenemos dos botones, y también tenemos el elemento de salida. Si recuerdas, no definimos nada relacionado con los roles allí. Solo eran botones y una entrada. Así que echemos un vistazo al árbol de accesibilidad por un momento. La consulta ByRoll es el mejor tipo de consulta, ya que se basa en los árboles de accesibilidad. Nos brinda el contexto más completo y el mayor poder para crear las mejores pruebas y las pruebas más confiables. Podemos ver que tenemos roles allí para encontrar aquí. Tenemos el rol de encabezado, tenemos el rol de deslizador, tenemos el botón, el estado y otro botón. Pero no definimos todos estos en nuestro JSX. Esto significa que en algún lugar del proceso de creación del componente, se agregaron estos roles. Entonces, para comprender la implementación de ByRoll, primero debemos comprender la especificación de los roles. ¿Qué significa incluso un rol? Todos estos roles están definidos en el Consorcio World Wide Web, que es un grupo que define las especificaciones para la web. Y específicamente, el área de rol es una parte de la especificación de área, que es accesible en todas las aplicaciones de Internet. Un rol agrega información semántica para las tecnologías de asistencia, y puede ser definido explícitamente o implícitamente. Entonces, cuando decimos que un rol está definido explícitamente, ¿qué significa? Literalmente significa que estamos escribiendo el atributo rol igual a alerta, lo que significa que quiero definir que este div tiene un rol específico. Entonces, ¿cómo consultamos elementos por su rol explícito en la biblioteca de pruebas? Cuando escribimos screen.getByRoll('button'), estamos transformando esta consulta específica a un document querySelectorAll con el rol de botón.
6. Understanding Implicit Roles
Un rol implícito significa que el rol se define de manera implícita. HTML semántico proporciona más información al usuario que el propio elemento. Mapeamos los tipos de elementos a sus roles implícitos. La consulta getByRole combina roles explícitos e implícitos para encontrar elementos. Transformamos la consulta getByRole en un selector de consulta de documento con el rol y los tipos de elementos especificados.
Así que eso es bastante simple, ¿verdad? Nada extravagante aquí, no hay magia, esto es lo que estamos haciendo. Por otro lado, un rol implícito es mucho más complejo. Un rol implícito significa que el rol se define de manera implícita. ¿Y cómo puede definirse un rol de manera implícita? Tenemos la suerte de tener HTML semántico y, por lo general, el HTML semántico proporciona más información al usuario que el propio elemento en sí.
Así que podemos ver que aquí tenemos un ejemplo de tipos de café. Tenemos ul y también tenemos algunas listas. En HTML semántico, ul implica el rol de lista y li implica el rol de elemento de lista. Esto significa que en la biblioteca de pruebas necesitamos tener algún tipo de mapeo entre un tipo de elemento y ese rol implícito específico para ese elemento. Y tenemos algo así. Entonces, cada vez que escribes getByRole botón, transformamos esta consulta en esto. Selector de consulta de documento rol input resumen botón. Todos estos tipos de elementos pueden implicar el rol de botón.
¿Qué obtenemos si combinamos el rol explícito y el rol implícito? Cada vez que escribes screen.getByRole botón, en el fondo lo que sucede es que estamos escribiendo selector de consulta de documento rol. Dame todos los elementos con el rol explícito de botón y también dame los tipos de elemento input, resumen y botón. ¿Entonces me estás diciendo que getByRole es simplemente selector de consulta de rol? Bueno, no exactamente, y aquí es donde se complica. Volvamos al JSX por un segundo y repasemos la consulta que acabamos de escribir, línea por línea o parte por parte para entender qué obtenemos. Este es nuestro JSX y repasemos la consulta parte por parte. Al principio, intentamos consultar todos los elementos con el rol explícito de botón y podemos ver que no tenemos elementos con un rol explícito de botón, así que pasamos a la siguiente parte. Ahora estamos buscando elementos del tipo input. Podemos ver que tenemos uno, así que lo guardamos aparte. A continuación, buscamos elementos del tipo resumen. No tenemos ninguno. Y por último, buscamos elementos del tipo botón y tenemos dos de esos. Esto significa que como resultado de esta consulta específica, obtenemos tres elementos.
7. Understanding Implicit Role Complications
No todos los inputs implican el rol de botón. El input de tipo imagen implica el rol de botón. El rol implícito es complicado. Construir un selector para todos los casos es difícil de mantener. Filtra los elementos por atributos específicos para encontrar el elemento deseado.
Así que tenemos un botón con el signo menos, tenemos otro botón con el signo más, y también tenemos un input de tipo rango. ¿Qué piensas? ¿Un input de tipo rango es realmente un botón? Bueno, no exactamente. No todos los inputs implican el rol de botón. Este input específico de tipo rango implica el rol de deslizador.
Entonces, ¿qué estoy diciendo? Esta consulta probablemente debería ser mejor. Selector de consulta de documento o input tipo botón diciendo dame todos los inputs con el tipo porque probablemente sean botones. Y tal vez sepas a dónde voy con esto. Pero juguemos un juego. ¿Cuál es el rol implícito para el input de tipo imagen? ¿Es A, una imagen, B, genérico, C, botón o D, sin rol implícito? Tomemos cinco segundos y veamos quién tiene razón. Vale, si elegiste imagen, genérico o sin rol implícito, lo siento, pero estás equivocado. La respuesta correcta para este caso es el rol de botón. El input de tipo imagen, de hecho, implica el rol de botón.
Entonces, lo que estoy tratando de decir aquí es que el rol implícito es algo complicado. Y construir un selector que se ajuste a todos los casos será difícil de mantener para nosotros en la biblioteca de pruebas. Entonces, lo que hacemos después de obtener la lista de todos los elementos que responden a la consulta, la consulta amplia, iteramos sobre los elementos y tratamos de filtrarlos por los atributos específicos que se mapean al rol implícito específico. Y después de filtrar, esta es la lista que tenemos. Tenemos dos botones. Pero aún no es el elemento que queríamos. Nos olvidamos de una parte específica de la consulta. Y esa parte era el nombre accesible. Estamos buscando el botón específico con el nombre accesible plus. Y es importante saber que un nombre accesible no es el atributo name. Y lo digo porque nos encontramos con un problema al mes diciendo algo como, agregué el atributo name, pero aún no puedo consultar ese elemento específico. Eso se debe a que un nombre accesible puede ser implícito desde el contenido, lo cual se llama nombre desde el contenido, o puede ser implícito desde la semántica ARIA, diciendo ARIA label, ARIA label by y eso se llama nombre desde el autor. Afortunadamente para nosotros, para el elemento botón, se implica desde el contenido. Entonces, cuando decimos plus, sabemos que esto se implica desde el contenido. Y ahora tenemos nuestro elemento. Aquí ocurre una última cosa.
8. Querying Elements and Test Clean Up
Cada vez que consultas un elemento, verificamos su visibilidad y accesibilidad. Iteramos hacia arriba en el árbol para verificar si hay ancestros ocultos. Construimos una consulta basada en el rol, filtramos los elementos con el rol implícito incorrecto, calculamos el nombre y la descripción accesibles, y verificamos la accesibilidad. La consulta getByRole es lenta pero poderosa. Estamos investigando mejoras de rendimiento. El evento Fire utiliza el envío nativo, pero se prefiere el evento de usuario. La limpieza se realiza después de cada prueba desmontando el árbol de React y eliminando los elementos para tener un nuevo comienzo.
Cada vez que intentas consultar un elemento, queremos verificar que en realidad sea visible y accesible para el usuario. Así que lo que estamos haciendo es iterar, iterar hacia arriba en el árbol, porque si alguno de los ancestros de este elemento estaba oculto, significa que este elemento también está oculto. Así que no queremos dártelo. Por lo tanto, si alguno de los ancestros o este elemento específico está oculto, no te lo devolveremos.
Resumiendo, lo primero que hacemos es construir una consulta basada en el rol explícito e implícito. Justo después de eso, filtramos los elementos que tienen el rol implícito incorrecto. Calculamos el nombre y la descripción accesibles. Y verificamos el árbol para accessibility. Es por eso que la consulta getByRole es la más lenta de todas las consultas. Y también es por eso que es la consulta recomendada. Es la consulta más poderosa, ya que imita el comportamiento del usuario lo mejor que podemos dentro de nuestra herramienta. También es importante saber que actualmente estamos investigando cómo podemos mejorar el performance cuello de botella que GetByRole causa en realidad.
Algunas cosas más. Evento Fire. Cada vez que intentamos disparar un evento, lo que sucede en el fondo es que en realidad estamos ejecutando el envío nativo del evento, diciendo que todo lo que estamos haciendo es cuando escribes fireevent.click es element.dispatch.event. Básicamente, esto es como funciona la especificación. Y el evento Fire es una forma de disparar eventos del DOM. Es una API de bajo nivel, lo que significa que en la mayoría de los casos de uso, no deberías usarlo. Probablemente deberías usar el evento de usuario en su lugar.
Y lo último que sucede dentro de nuestra prueba es que, siempre que tu entorno de prueba admita after each, estamos ejecutando la limpieza por ti. Así que si recuerdas esta línea de nuestra parte de renderizado donde agregamos referencias al contenedor y a la raíz a un array, lo que hacemos en la parte de limpieza es recorrer ese array y llamar a root.unmount para desmontar el árbol de React y también llamamos a document body.removeChild para darte un inicio fresco y limpio cada vez que abres o cada vez que comienzas una nueva ejecución de prueba. Volviendo a la historia con la que comenzamos. Como probablemente puedas adivinar, aproximadamente una semana después, el commit completo en todas mis solicitudes de extracción fue revertido por completo. No quedó ni un solo carácter que yo haya escrito en el código base. Y al principio pensé que era mi culpa. Quería esconderme, nunca más quería contribuir a código abierto, pero todo este proceso me enseñó una lección valiosa. He estado usando Testing Library durante casi un año antes de hacer esta contribución, y ni siquiera sabía cómo funciona en realidad. Después de esta charla, eres más que capaz de contribuir a Testing Library por ti mismo. Si necesitas ayuda o tienes alguna pregunta, estoy disponible en todas estas plataformas. Háblame. Espero que hayas aprendido algo y muchas gracias por escuchar. Espero verte a todos cara a cara pronto. Gracias.
Comments