¿Buscas nuestro logo?
Aquí te dejamos una copia, pero si necesitas más opciones o quieres conocer más, visita nuestra área de marca.
Conoce nuestra marca.¿Buscas nuestro logo?
Aquí te dejamos una copia, pero si necesitas más opciones o quieres conocer más, visita nuestra área de marca.
Conoce nuestra marca.dev
Abel Tamayo 30/06/2021 Cargando comentarios…
Durante los últimos años se han introducido cambios significativos en el ecosistema de React, como la llegada de los hooks o la adopción de React Testing Library como nuevo standard para tests. Esto hace necesario nuevos métodos para testear las aplicaciones y conseguir una buena cobertura.
Para ejemplificarlo, vamos a basarnos en un proyecto sencillo de chat (repositorio en Github) que, a pesar de ser pequeño, cubre una buena cantidad de casos. Por supuesto, una lectura completa de la documentación de Jest y React Testing Library es muy recomendable también.
Proyecto creado con create-react-app o con Jest y React Testing-library y convenientemente configurado para ejecutar tests.
@testing-library/user-event no viene instalada por defecto con proyectos create-react-app
React testing library introduce una nueva API cuyos principales métodos son render y screen. Con render podemos montar un componente de la forma habitual en React y con el singleton screen podemos leer lo que hay en él.
Empezando por el ejemplo más básico (aunque necesario para conseguir cobertura) para un componente sin funcionalidad podemos probar que se renderiza bien y que tiene alguna característica determinada.
El componente para el logotipo de la aplicación tiene solo eso: un texto con la palabra “chat” y ninguna funcionalidad. Podemos probarlo de una manera muy sencilla así:
Lo que hacemos en dos pasos es primero renderizar aisladamente el componente Logo, y segundo buscar en screen las características que lo definen, como el que haya un texto que coincida con “CHAT”. No hace falta guardar en una constante el contenido de render porque screen siempre tiene disponible lo último que se haya renderizado.
Hasta este momento solo necesitamos los selectores tipo “get” de React testing Library. Son getByRole, getByLabelText, getByPlaceholderText, getByText o getByDisplayValue y lo que hacen es buscar en el contenedor dado, un elemento que tenga las características de texto pasadas como parámetro con un string o con un regexp. Podemos leer más acerca de los selectores de React Testing Library aquí.
La aplicación chat-room utiliza react-query en forma de custom hooks, que es la manera recomendada para mejorar la legibilidad y facilitar los tests.
Un caso ligeramente más complicado que el que acabamos de ver es este de un componente que lee datos obtenidos de un hook useParticipants, que únicamente le devuelve una lista de participantes activos en ese momento. El contenido de este hook lo veremos en un momento pero por el momento, centrémonos en probar el componente que lo usa.
Que podemos probar así:
Recordemos que estos son tests unitarios así que probamos que el componente funciona mockeando todas sus dependencias. En este caso, vemos que mockeamos el hook useParticipants con jest.mock(‘../hooks/useParticipants’) y en un paso posterior mockeamos su implementación para que devuelva siempre unos datos conocidos: useParticipants.mockImplementation(() => participants). De esta manera, luego podemos asertar con un selector que la lista de participantes será de longitud 3, ya que es lo que le hemos pasado inicialmente. Así estamos probando el listado de participantes y no su dependencia.
Notar también que en lugar de seleccionar por el texto que contiene el elemento estamos usando los selectores getByTestId y getAllByTestId, que buscan el componente html cuyo atributo data-testid coincida con el previsto.
Algo más de imaginación y meticulosidad requiere probar componentes con interactividad que permitan al usuario hacer click, escribir, etc… Para estos casos React Testing Library expone una api como userEvent que va a cubrir todas nuestras necesidades.
El componente MessageInput se compone de un input de texto y un botón, que al pulsarlo envía el texto a un servicio que lo publica en el chat.
De entrada vemos que el componente depende de dos hooks que son useCurrentUser y useMutatePostMessage, así que tendremos que mockearlos en el test. Además, vemos que hay diferentes casos contemplados, como que el usuario no esté logado, que se intente enviar un mensaje sin texto o que se use la tecla intro en lugar del botón. Si queremos una buena cobertura, tendremos que contemplar todos estos casos uno a uno.
Lo más novedoso en esta parte seguramente es la técnica para mockear una función y después comprobar que ha sido llamada tras un evento y el uso de la librería userEvent, que nos proporciona una manera realista de simular eventos como los que provocaría un usuario, como hacer click en un elemento, hacer hover, rellenar un campo de un formulario tecla a tecla, etc… Disponemos también de la función fireEvent, pero actualmente está recomendado usar userEvent por ser la más realista de las dos.
Por supuesto que las pruebas para componentes son una habilidad compleja y extensa pero hemos cubierto bastante terreno con estas nuevas técnicas y APIs y seguramente nos dejen cercanos al 100% de cobertura siempre que nos organicemos bien y tengamos el código bien abstraído. Como decíamos al principio, lo recomendable es tener los hooks aislados como custom hooks para simplificar su mockeado en los componentes y para probarlos por separado como vamos a hacer ahora.
En este caso concreto, hemos abstraído el hook de react-query que hace la petición para obtener las llamadas que nos proporciona el servicio de servidor.
Un hook muy sencillo que se resuelve en una sola sentencia, pero probar hooks, que por norma no pueden ejecutarse fuera de los componentes, es un reto y vamos a necesitar una librería auxiliar para conseguirlo, react-hooks, que ya instalamos en los primeros pasos de este tutorial.
Lo primero como de costumbre es moquear la dependencia, en este caso api/getMessajes.js, que probaremos aparte. Como getMessages es una promesa de fetch, la mockeamos acordemente: getMessages.mockImplementation(() => Promise.resolve(messages)). Puesto que vamos a trabajar con promesas, hacemos que el bloque entero sea async para poder esperar a que se resuelvan con await.
Importamos renderHook, que es la librería standard de facto para probar hooks aisladamente e instanciamos el hook en cuestión con la línea const { result } = renderHook(() => useMessages()), que nos devuelve varios posibles helpers y valores, el más importante de ellos result, que contiene el resultado real de ejecutar dicho hook.
Por último, como hemos controlado que se devuelvan tres mensajes, comprobamos que esto sea así y con eso damos por probado el hook.
En este caso particular se prueba a reimplementar con nuevos datos porque queremos comprobar que nuestro hook no pierde los iniciales, sino que se suman, pero esto es una característica propia de este hook y la manera en la que está implementado.
Son ligeramente diferentes a un hook de consulta. Por ejemplo, para el hook que hace un POST de un mensaje:
Lo probamos similarmente así:
Resumiendo, el hook devuelve una función que el componente puede utilizar para ordenar el post de un mensaje así que comprobamos que, efectivamente, el hook devuelve una función y que utilizarla llama a la función esperada api/postMessage.js y con los parámetros esperados.
Partíamos de una aplicación con múltiples capas de abstracción y con esta llegamos finalmente a la parte donde realmente se hace la llamada.
Que probamos así:
Hemos tenido que mockear la función window.fetch y una de las posibles maneras es con jest.spyOn para luego sustituirla por una nueva implementación sobre la que llamar a su propiedad then y que nos devuelva el dato que nosotros controlamos.
Lo que hemos visto hasta ahora eran tests unitarios, lo cual significa que, dado un determinado módulo de forma aislada, funciona de la forma esperada. Sin embargo, a un nivel superior, estas piezas separadas se encontrarán unidas de una forma organizada y la interacción entre ellas es lo que probamos con los tests de integración, que se colocan a un nivel de abstracción superior a los unitarios.
Una buena forma de distribuir los tests de integración es uno por página/pantalla/ruta si nuestra aplicación incluye varias de ellas. Como en nuestro caso solo hay una, podemos poner los tests adyacentes al componente principal App.js.
Nuestro App.js tiene solo unas pocas líneas para importar los componentes de la aplicación:
El archivo de prueba es mucho más largo porque estamos probando todas las interacciones entre usuario y componentes.
La filosofía que adoptamos consiste en mockear todas las llamadas a la API, ya que son dependencias y lo único que queremos comprobar es que se llega a hacer la llamada y a continuación darles una implementación específica para cada caso dependiendo de a qué resultados de la API queremos que reaccione la aplicación. Por último, simulamos la interacción del usuario y vemos si el resultado que obtenemos es el esperado.
Probamos desde el nivel de abstracción más alto, como es la interacción del usuario, hasta el nivel de abstracción más bajo, como es el resultado de llamar a las APIs (pero moqueadas para evitar dependencias externas). Los elementos de abstracción intermedio como los hooks debemos dejar sin mockear, ya que así no estaríamos probando su integración.
De nuevo hacemos una separación por estados y por la parte de funcionalidad que estamos probando:
Puede parecer que algunas de estas preguntas ya estaban contestadas con los tests unitarios pero no es así: con los unitarios probamos la interacción de todas las capas de la aplicación entre sí. Por ejemplo, el test unitario de LoginController comprueba si aparece la pantalla de login en base al valor almacenado del usuario actual (abstraído y mockeado en el hook useCurrentUser) sin embargo al probarlo en integración, no mockeamos el hook sino que simulamos diferentes escenarios con la respuesta del servicio y comprobamos si el dato pasa por todas las capas hasta mostrar o no la pantalla de login.
En este caso hemos abstraído la simulación de un usuario logueándose con unos datos parametrizados al ser una operación que hemos repetido varias veces y también hemos configurado la caché de react-query para que se resetee después de cada test (necesario sólo para proyectos con react-query).
Todo este proceso de testeo lo hemos llevado a cabo mientras se ejecutan los tests para poder ver el resultado de cada uno y ajustar dependiendo de si pasan o no sin errores con el conocido yarn test o npm test pero sabiendo la gran capacidad de configuración de Jest, podemos modificar la configuración y obtener además información sobre su cobertura, controlar la forma en que recibimos los resultados, imponer rangos mínimos de seguridad…
Podemos crear un archivo de configuración en la raíz del proyecto con las nuevas opciones de ejecución de Jest, extendiendo la configuración original de create-react-app e incluyendo nuevas opciones de cobertura.
Ahora podemos modificar fácilmente los scripts en package.json para incluir uno adicional que nos dé cobertura además del original que solo ejecuta los tests, ambos utilizando el nuevo archivo de configuración.
Ahora con yarn test:coverage o npm test:coverage obtendremos un resultado con la cobertura de cada archivo por sus tests asociados.
Como vemos, el informe incluye un resumen del porcentaje de cobertura, fallos y si se cumple el margen de seguridad mínimo. Es un arduo camino hacia el 100% pero organizándonos bien y con estas técnicas podemos acercarnos bastante.
En resumen, React Testing Library nos ofrece un nuevo enfoque a más alto nivel sobre los tests, válido tanto para unitarios como para integración y al combinarlos podemos conseguir una cobertura de tests con la que que confirmar que estamos desarrollando sobre seguro en proyectos de cualquier tamaño.
Los comentarios serán moderados. Serán visibles si aportan un argumento constructivo. Si no estás de acuerdo con algún punto, por favor, muestra tus opiniones de manera educada.
Usamos cookies propias y de terceros con fines analíticos y de personalización. Las puedes activar, configurar o rechazar. Configurar o rechazar.
Cuéntanos qué te parece.