Video Summary and Transcription
Esta charla explora las pruebas unitarias en aplicaciones Angular, abordando temas como las pruebas de aplicaciones front-end, los detalles de las pruebas en Angular, las mejores prácticas y los recursos educativos. Se discute la anatomía de una prueba unitaria tanto en Jasmine como en Jest, la configuración y las pruebas iniciales en Angular, las pruebas de interacción del usuario y los controladores de eventos, las pruebas de la salida renderizada y la detección de cambios, y las pruebas unitarias de componentes padres con componentes hijos. También se destacan las mejores prácticas como el uso de dobles de prueba, las pruebas de componentes con inyección de dependencias y las consideraciones para las pruebas unitarias. Se enfatiza que la cobertura de código es una métrica que no garantiza un código libre de errores.
1. Introducción a las pruebas unitarias en aplicaciones de Angular
Bienvenidos a mi charla sobre las pruebas unitarias en aplicaciones de Angular. Es crucial escribir pruebas para tu código para asegurarte de que se ejecute correctamente y haga lo que se supone que debe hacer. Exploraremos formas de probar aplicaciones front-end, aspectos específicos de las pruebas en Angular, mejores prácticas aplicables a otros frameworks, qué probar, qué no probar y recursos educativos. Para las pruebas front-end, Jasmine y Jest son frameworks populares, mientras que Cypress, Selenium y Playwright se utilizan para las pruebas end-to-end. Me centraré en Jasmine para las pruebas unitarias en Angular. Las pruebas de integración difuminan la línea entre las pruebas unitarias y las pruebas end-to-end. Angular se centra principalmente en probar clases y crear instancias. Veamos las herramientas de Angular para las pruebas y la creación de instancias.
Hola a todos, bienvenidos a mi charla sobre las pruebas unitarias en aplicaciones de Angular. Primero, una breve introducción sobre mí. Mi nombre es Filip Voska, soy de Croacia y trabajo en Infinium como líder de equipo de JavaScript. A lo largo de los años, una cosa que he aprendido es que realmente debes escribir pruebas para tu código. Nadie escribe código perfecto y quieres tener cierta confianza en que se está ejecutando correctamente y haciendo lo que se supone que debe hacer, y para eso se utilizan las pruebas. Así que rápidamente repasemos los temas, veremos algunas formas de probar aplicaciones front-end, algunos aspectos específicos de las pruebas en Angular, luego en la tercera parte veremos algunas mejores prácticas que no están necesariamente relacionadas con Angular, pero son conceptos que se pueden aplicar en otros frameworks, y al final, tendremos una visión general de algunas cosas que debes probar, que no debes probar y cuáles son los siguientes pasos y los recursos educativos disponibles.
Ok, primero veamos qué probar. Si buscas en línea, probablemente, si buscas pruebas, verás algo relacionado con la pirámide de pruebas y es lo que describe la proporción entre las pruebas unitarias, de integración y end-to-end. Algunos, en la versión más común, dicen que debes tener la mayor cantidad de pruebas unitarias, luego un poco menos de pruebas de integración y luego aún menos pruebas end-to-end. Ahora, hay variaciones de esto, donde algunos dicen que deberías tener algo como esto, que es como un reloj de arena de pruebas, donde deberías tener, digamos, la misma cantidad de pruebas unitarias y end-to-end, pero un poco menos de pruebas de integración. Y luego también encontrarás esta forma, que es algún tipo de jarrón donde puedes poner flores. Y sí, esas son todas diferentes filosofías, y eso es todo un tema aparte. No entraremos en detalles sobre eso. Solo veremos lo que importa para Angular.
Y para las aplicaciones front-end, generalmente tienes la opción entre Jasmine y Jest. Esos son los dos frameworks de pruebas más populares. Con Jasmine, también usarías Karma como ejecutor de pruebas. Jest es una solución todo en uno. Y Angular viene con Jasmine y Karma listos para usar, pero también es bastante fácil de usar Jest con él. Luego, para las pruebas end-to-end, tienes cosas como Cypress, Selenium, Playwright. Y todas estas son formas de ejecutar un navegador automatizado donde es un navegador real ejecutando tu código y estás simulando el comportamiento del usuario. Así que hoy me centraré en Jasmine para las pruebas unitarias. No cubriré las pruebas end-to-end o de integración. Ahora, las pruebas de integración realmente se pueden hacer con cualquiera de estas herramientas porque la línea entre las pruebas end-to-end, de integración y unitarias es un poco borrosa. Depende de cómo definas una unidad y cuántas unidades están involucradas en una prueba. Entonces, ¿qué es una unidad en el contexto de Angular? Bueno, Angular está compuesto principalmente por diferentes clases y esas clases pueden ser componentes, directivas, pipes, módulos, servicios, etc. Y también tienes funciones como funciones auxiliares regulares que podrías tener. Así que principalmente estamos hablando de probar clases en Angular y crear instancias de esas clases. Veamos algunas de las herramientas que Angular nos proporciona para facilitar las pruebas y la creación de esas instancias. Esto es lo que
2. Understanding Angular Components and Unit Testing
Angular consiste en un componente de clase con entradas y salidas para la comunicación con los componentes principales. Tenemos un ejemplo simple con un botón que incrementa un contador y emite un evento al componente principal. Vamos a explorar la anatomía de una prueba unitaria tanto en Jasmine como en Jest. Implica definir un conjunto de pruebas, configurar un estado inicial y escribir pruebas unitarias individuales.
componente en el que estaremos trabajando. Es bastante simple. Para aquellos que tal vez no estén familiarizados con Angular, Angular consiste en un componente de clase que tiene entradas y salidas, y estas son las formas en que el componente puede comunicarse con los componentes principales. Entonces, el componente principal puede pasar algo a través de una entrada al componente secundario y el componente secundario puede emitir un evento de vuelta al componente principal utilizando una salida. En nuestro ejemplo, tenemos un componente muy simple donde se puede ver en la plantilla en la línea 4 que mostramos la cantidad de veces que se ha hecho clic en el botón, y en la línea 6 tenemos un botón con un controlador de clic adjunto que llama a un método y ese método está definido en la clase y simplemente incrementa el contador y emite el evento al componente principal. Entonces, este es uno de los componentes más básicos que podrías tener en Angular. Así que echemos un vistazo a la anatomía de una prueba unitaria. Y esto es realmente lo mismo tanto en Jasmine como en Jest. Hay algunas otras diferencias entre ellos, pero esto es realmente lo mismo y también es lo mismo en muchos otros lenguajes. Primero, definirías un conjunto de pruebas. Entonces, estamos definiendo un conjunto de pruebas para el componente de contador, usamos la función describe para eso. Luego tenemos algo... Usualmente tenemos beforeEach. beforeEach es un fragmento de código que se ejecutará antes de cada prueba, cada prueba individual, y aquí configurarías algún estado y algún estado inicial. Luego, entre las líneas cuatro y seis, finalmente tenemos una prueba unitaria individual. Está utilizando la función it. Es un nombre un poco extraño, pero se llama así porque la forma en que se supone que debes leer esto es `el componente de contador debería hacer algo`, por eso la función se llama it.
3. Angular Testing Setup and Initial Unit Tests
Exploremos el andamiaje básico de la prueba agregando elementos específicos de Angular. Tenemos el componente y el fixture, y configuramos un módulo de prueba. Creamos el fixture y obtenemos la instancia del componente. Tenemos la primera prueba unitaria para el componente Counter, verificando la inicialización y el estado inicial. Probamos los elementos pre y button renderizados, y verificamos el estado inicial del contador.
De acuerdo, expandamos un poco este andamiaje básico de la prueba agregando algunos elementos específicos de Angular. Aquí tenemos el componente y el fixture. El fixture está relacionado con el componente, por lo que el fixture es algo que tiene algunos métodos y ayudantes adicionales que facilitan la interacción con el componente en las pruebas y nos permite consultar algunas cosas, como veremos más adelante. Y el componente es la instancia de la clase del componente en sí. Así que tendremos referencias a dos de ellos y los usaremos bastante en las pruebas. Luego, en las líneas 11 a 13, configuramos un módulo de prueba. Los componentes de Angular se ejecutan dentro de un módulo tanto en tiempo de ejecución como en las pruebas. Aquí estamos configurando un módulo de prueba dentro del cual se ejecutará el componente. Aquí definimos cualquier dependencia que el componente pueda tener. En este caso, estamos tratando con un componente muy simple que no tiene ninguna dependencia, por lo que simplemente importamos ese componente. Luego, en las líneas 15 y 16, creamos el fixture utilizando 'CreateComponent' y obtenemos la instancia del componente. La línea 17 es algo bastante específico de Angular, donde llamamos a 'DetectChanges'. Esto es lo que desencadena el proceso de volver a renderizar. Entonces, en tiempo de ejecución, cuando la aplicación se está ejecutando, la detección de cambios, que es el proceso encargado de volver a renderizar cuando es necesario, es automática, pero en las pruebas tienes que hacerlo de manera un poco más manual. Ahora, finalmente, entre las líneas 20 y 22, tenemos nuestra primera prueba unitaria para nuestro componente Counter. Simplemente afirmamos que el componente está inicializado, que la instancia de la clase existe. Por lo tanto, es un valor verdadero, el componente. Ahora probemos algún estado inicial. Usaremos nuestro método 'beforeEach' para consultar el elemento 'pre' y el elemento 'button'. Y los almacenaremos en algunas variables en el conjunto de pruebas, porque los usaremos en varios lugares, por lo que son más reutilizables. Aquí podemos ver que usamos 'fixture.debugElement.query(By.css)' y obtenemos el elemento 'pre'. Hacemos algo similar para el botón, y para el elemento 'pre' también leemos su elemento nativo. Luego, finalmente, en las líneas, ahora tenemos tres pruebas unitarias, por lo que verificamos que el elemento 'pre' renderizado existe, verificamos que el elemento 'button' renderizado existe, esos son los dos primeros tests, y en la prueba final verificamos que el estado inicial del contador esté configurado en uno, y eso es solo una prueba básica para ver que todo funcione inicialmente. Ahora queremos verificar qué se renderiza. Es bastante importante verificar qué se renderiza como HTML final, porque alguien podría eliminar toda la plantilla del componente, y si solo estuvieras probando el estado del componente, como verificar los valores de las propiedades de clase, todas tus pruebas seguirían pasando incluso si alguien elimina toda la plantilla. Por eso es importante verificar qué se renderiza y por eso es importante hacer clic en los elementos, etc. Veremos más sobre eso. Pero sí, aquí recuerda que creamos nuestro elemento 'pre'. Es un elemento HTML y simplemente verificamos que el contenido de texto sea 'times clicked one', y ese también es el estado inicial.
4. Prueba de interacción del usuario y controladores de eventos
Cuando se prueba la interacción del usuario, es crucial activar los controladores de eventos en lugar de llamar directamente a los métodos del controlador. Esto asegura que la prueba falle si el botón que se hace clic no existe.
Así es como se relaciona con la plantilla que tenemos. Ahora, al igual que queremos verificar qué se renderiza, también queremos verificar qué sucede cuando interactuamos con el usuario, y ahora es importante cuando hacemos la interacción del usuario testing, no debes llamar directamente a los métodos del controlador en la clase. Lo que debes hacer es activar los controladores de eventos. Por razones similares a por qué quieres verificar qué se renderiza, alguien podría eliminar tu plantilla. Si solo llamas directamente al controlador de clic, aún pasará, pero si realmente intentas hacer clic en el botón y el botón no existe,
5. Testing Rendered Output and Change Detection
En las pruebas unitarias de aplicaciones Angular, es importante activar manualmente la detección de cambios después de simular la interacción del usuario. Esto asegura que la salida renderizada coincida con los valores esperados. Al verificar lo que se renderiza, puedes asegurarte de que todo funcione correctamente.
la prueba fallará, ¿de acuerdo? Por eso es importante. Sí, aquí en la línea siete y ocho, afirmamos que el contador es inicialmente uno y el texto renderizado es 'times clicked one'. Luego hacemos clic, luego verificamos. La forma en que hacemos clic es activando el controlador de eventos programáticamente. Esto se relaciona con el controlador de clic que adjuntamos, y esta prueba pasó. Ahora verificamos que el contador sea dos, pero verás que parece que tengo un error tipográfico aquí. Así que verifiqué que el contador del componente sea dos, pero la prueba pasa y lo que realmente se renderiza es que el texto es todavía 'times clicked one', no 'times clicked two'. Y esto se debe a que, como mencioné, la detección de cambios, que es un proceso para volver a renderizar, no se ejecuta automáticamente en las pruebas como lo hace en runtime, cuando la aplicación se está ejecutando realmente. Por eso tienes que llamarlo manualmente después de simular alguna interacción del usuario. Entonces, si activas alguna interacción del usuario programáticamente, debes llamar a 'detectChanges' para volver a renderizar. Ahora, la prueba fallará porque 'times clicked one' no es lo que realmente se renderiza. Lo que realmente se renderiza es 'times clicked two'. Y puedes ver en el registro de errores, obtienes un mensaje agradable donde dice cuál era el valor esperado y cuál era el valor actual. Ahora lo arreglamos actualizando uno a dos, por lo que esta prueba es válida. Entonces, las dos cosas más importantes aquí es que debes saber cuándo debes volver a renderizar en las pruebas. Es un poco tedioso, pero debes hacerlo. Y la segunda cosa es verificar lo que se renderiza para asegurarte completamente de que todo funcione correctamente.
6. Unit Testing Parent Component with Child Component
Ahora estamos escribiendo pruebas unitarias para nuestro componente de contador. Probamos la plantilla de un componente padre, verificando si utiliza correctamente el componente hijo. Utilizamos una consulta por CSS para seleccionar el componente hijo, obtener la instancia del componente y verificar que la entrada del contador esté establecida en 100. También podemos probar la reacción del componente padre al evento de cambio del contador mediante la consola.log del nuevo valor. Utilizamos el espía de Jasmine para verificar si se ha llamado a console.log con el valor esperado.
Ahora estamos escribiendo pruebas unitarias para nuestro componente de contador. Pero digamos que esto es, ahora estamos viendo la plantilla de un componente padre. Entonces queremos probar cómo nuestro componente padre utiliza este componente hijo. Y esto es ahora una prueba unitaria de un componente padre, donde podemos obtener la instancia del componente hijo. Aquí puedes ver que usaremos una consulta por CSS para seleccionar el contador de test.js y el contador de test.js es el selector de nuestro componente hijo. Puedes ver en la plantilla que se está utilizando de esa manera como un selector de elemento. Luego obtenemos la instancia del componente y verificamos que la entrada del contador esté establecida en 100. Por lo tanto, básicamente estamos verificando que nuestra plantilla esté haciendo lo que esperamos que haga, estableciendo el contador en 100 inicialmente. También podemos probar en la dirección opuesta. Entonces, si nuestro componente padre quiere reaccionar al evento de cambio del contador, también podemos escribir pruebas para eso. Digamos que en el cambio del contador, simplemente hacemos console.log del nuevo valor en el componente padre. Este evento de dólar es una pieza especial de sintaxis, pero este será el valor con el que se emite el evento. En este caso, será un número que se emite en el cambio del contador y ese es el valor que se pasa al método del controlador. Por lo tanto, aquí la prueba se expande un poco más porque ahora estamos usando spy-on. Spy-on es algo de Jasmine. Es muy similar en Jest también. Básicamente, tomas console.log y espías que se haya llamado. En la línea 3, configuramos el espía y en la línea 5, esperamos que no se haya llamado. En la línea 7, activamos el controlador de eventos para, digamos, emitir 101 como el nuevo valor del contador, y luego esperamos que se haya llamado console.log una vez con el valor 101. Es importante verificar cuántas veces se llamó algo anteriormente, cuántas veces se llamó después y con qué valor se llamó. Y todo esto es bastante agradable... por lo general, quieres verificar que algo se haya llamado una vez y con algún valor, y por eso tengo este buen método para que se haya llamado una vez con algo. Y así es como se relaciona el evento y cómo se relaciona el nombre del evento y el controlador entre la plantilla.
7. Best Practices: Test Doubles
Las pruebas dobles son una mejor práctica en las pruebas unitarias de aplicaciones Angular. Cuando tu componente depende de un servicio que realiza llamadas a API, puedes crear una prueba doble del servicio. Esta prueba doble, también conocida como mock, tiene los mismos métodos que el servicio real pero devuelve datos simulados en lugar de realizar llamadas reales a la API.
y la prueba. Bien, veamos algunas de las mejores prácticas. Pruebas dobles. Por lo general, si tu aplicación depende de algún, digamos, servicio, y los servicios en Angular son simplemente clases que ofrecen métodos y se utilizan principalmente para obtener datos. Y aquí nuestro servicio de usuario está obteniendo... como haciendo una llamada a la API y obteniendo la lista de usuarios. Sin embargo, cuando estás probando a nivel de unidad, realmente no quieres hacer llamadas a la API. Y si tu componente depende del servicio de usuario, quieres evitar hacer llamadas a la API. Entonces, para ese propósito, crearías una prueba doble del servicio de usuario. Normalmente lo llamamos aquí servicio de usuario de prueba, pero podrías llamarlo algo como servicio de usuario simulado o algo así. Y aquí defines todos los mismos métodos que tiene el servicio real, pero no realiza una
8. Testing Components and Best Practices
Angular utiliza la inyección de dependencias para inyectar servicios en componentes. En las pruebas unitarias, proporcionamos una versión de prueba doble del servicio. El mismo enfoque se puede aplicar a los componentes. Al utilizar pruebas dobles, podemos probar un componente principal sin integrarlo con sus dependencias. Angular CDK proporciona arneses, que ofrecen métodos adicionales para probar componentes. La cobertura es una métrica que muestra cuánto del código ha sido ejecutado por las pruebas. Sin embargo, no incluye las plantillas HTML y una cobertura del 100% no garantiza un código libre de errores.
llamada API real, pero devuelve algunos data simulados. Y de esta manera, sí, así es como se utiliza en algunos componente de tabla de usuarios, por ejemplo. Entonces, Angular utiliza la inyección de dependencias. Y este componente inyectaría el servicio de usuarios. Pero cuando estamos escribiendo pruebas, proporcionamos servicio de usuarios de prueba en lugar del servicio de usuarios real. Entonces, el componente todavía espera que obtendrá una instancia del servicio de usuarios como . Pero cuando se ejecutan las pruebas, obtienes la versión simulada, la prueba doble, la versión de prueba del servicio, como quieras llamarlo. Y puedes hacer prácticamente lo mismo para un componente. Entonces, el componente también tiene algunos métodos. Tiene algunas dependencias. Puedes crear una versión de prueba del componente, para lo cual es importante que tenga el mismo selector. Pero no tiene que tener la plantilla compleja, y no tiene que tener ninguna lógica de implementación. Solo necesita tener todas las entradas y salidas. Y haces eso de manera muy similar. Entonces, si tu componente principal depende del componente contador, en las pruebas unitarias para el componente principal, importarías el componente contador de prueba. Y de esa manera no usas el componente real. Usas una simulación, y esto es lo que lo convierte en una prueba unitaria, no una prueba de integración. Entonces, no estás testing ambos componentes al mismo tiempo. No estás testing el componente principal y el componente contador al mismo tiempo. Solo estás testing el componente principal y usando una versión de prueba doble del contador.
El próximo tema son los arneses. Ahora, no entraré en detalles sobre los arneses, pero son algo que proporciona Angular CDK, y puedes definir un arnés para algún componente. Y ese arnés tendrá algunos métodos adicionales que son útiles para las pruebas. Por ejemplo, en un ejemplo de un checkbox de material, ahí tienes un arnés que facilita alternar el checkbox en las pruebas, etc. Pero, sí, no entraré en demasiados detalles. También puedes escribir arneses para tus propios componentes. Ahora, el último tema sobre las mejores prácticas es la cobertura. Entonces, la cobertura es una métrica que te dice, cuando recopilas todas tus pruebas, como cuánto del código ha sido cubierto. Y esto es, puedes obtener un informe con un aspecto agradable como este, y si observas un archivo específico, puedes ver qué partes del código se han ejecutado y cuáles no. En general, esto es algo bueno, pero no es algo que debas perseguir ciegamente, porque la cobertura en Angular no incluye las plantillas HTML, por lo que no obtendrás eso en la cobertura. Una cobertura del 100%, por supuesto, no significa que no haya errores, y dependiendo de la configuración, es posible que algunos archivos ni siquiera se incluyan en la cobertura, por lo que
9. Unit Testing Best Practices and Considerations
La cobertura de código a menudo es engañosa, pero a los gerentes de proyecto les gusta. Prueba la lógica de negocio, los servicios, los componentes principales de la interfaz de usuario, el manejo de datos sensibles, los ayudantes, los errores y los estados de carga. Prueba si algo se vuelve a renderizar correctamente. No pruebes bibliotecas de terceros ni detalles de implementación como métodos privados. Evita probar interacciones complejas entre unidades y flujos de interfaz de usuario complejos. Las pruebas unitarias son ideales para probar código más complejo que podría romperse. Considera la complejidad de configurar pruebas unitarias en comparación con el valor de la funcionalidad.
Obtenemos números incorrectos en realidad, y tenemos documentación al respecto. Entonces, sí, la cobertura de código a menudo es engañosa, pero es un número que a los gerentes de proyecto les gusta escuchar. Bueno, permíteme resumir algunos de estos temas. Entonces, ¿qué deberías probar unitariamente? Deberías probar la lógica de negocio, los servicios, los componentes principales de la interfaz de usuario, el manejo de datos sensibles, los ayudantes, los errores y los estados de carga, probar si algo se vuelve a renderizar correctamente y cualquier cosa realmente relacionada con el manejo de fechas y zonas horarias, porque es realmente, realmente doloroso de depurar, así que tener buenas pruebas unitarias para esas cosas, eso es realmente bueno, pero también hay algunas cosas que no deberías probar realmente unitariamente, como bibliotecas de terceros, detalles de implementación y lo que quiero decir con esto es que no deberías probar métodos privados, no deberías llamarlos directamente, siempre debes probar algo de la forma en que se usa, así que si estás probando un componente, pruébalo, escribe tu prueba unitaria como si fueras un componente padre, no llames a métodos privados. Tampoco deberías probar interacciones complejas entre unidades, eso son pruebas de integración, eso no es para pruebas unitarias, es un tipo de prueba diferente. Los flujos de interfaz de usuario complejos también son bastante difíciles de hacer con pruebas unitarias, especialmente si se trata de cosas como deslizar, etc., así que es mejor dejar eso para las pruebas de extremo a extremo. Si tienes algún código realmente trivial o algún código que está aquí solo temporalmente, no obtendrás mucho beneficio de las pruebas unitarias, es probable que ese código no se rompa. Quieres probar cosas que sean más complejas, que podrían romperse, y quieres asegurarte de que no se rompan. Y nuevamente, configurar pruebas unitarias a veces puede ser bastante complejo. Entonces, si el tiempo que lleva configurar una prueba unitaria es mucho mayor que el valor de la funcionalidad, entonces tal vez considera omitirlo y hacer otro tipo de prueba. Aquí, al final, dejaré esta breve lista de cosas que debes y no debes hacer. Puedes revisar esto, pero sí, es muy importante escribir. Creo que lo que más quiero enfatizar aquí es que debes escribir pruebas basadas en tus requisitos. De esta manera, las pruebas también serán una documentación de lo que tu código debería hacer. Y también es una documentación que se puede ejecutar para verificar que el código esté haciendo lo que se supone que debe hacer. Creo que esa es la nota más importante que debes tener en cuenta. Asegúrate de que tus requisitos estén cubiertos por las pruebas. Aquí hay algunos recursos adicionales que tienen. La documentación oficial de Angular para las pruebas es bastante buena. Cubre muchos aspectos. También tenemos el manual de Invenum sobre las pruebas de Angular y allí tenemos muchos casos de prueba diferentes, digamos recetas, sobre cómo debes probar algo. También puedes encontrar algunos cursos útiles en línea sobre pruebas. Aquí, enlazaré el curso de pruebas de Angular Academy. No tengo ninguna relación con ellos. Simplemente creo que tienen buen contenido. Eso es prácticamente todo. Gracias por tu atención. Sé que tal vez haya sido mucho, pero por favor, puedes volver y revisar algunas secciones si hay algo en lo que quieras echar otro vistazo. Estaré en el chat para responder cualquier pregunta y sí, gracias. Adiós.
Comments