Testing Unitario FrontEnd

Karla Cabañas

October 25, 2023

Introducción

Las pruebas unitarias son una parte fundamental en el desarrollo de aplicaciones para garantizar su funcionalidad y prevenir errores. En este artículo, te guiaré a través del proceso de creación de pruebas unitarias en una aplicación React con TypeScript, utilizando las herramientas Jest y React Testing Library, cabe destacar que ambas se complementan entre ellas.

Jest es un framework de pruebas desarrollado por Facebook. Su objetivo es hacer que escribir pruebas sea fácil y efectivo. Jest proporciona un entorno completo para escribir, ejecutar y analizar pruebas en proyectos JavaScript.

React Testing Library es una biblioteca que ayuda a realizar pruebas de componentes React. Se enfoca en cómo interactúa un usuario con la interfaz, en lugar de centrarse en la implementación interna de los componentes. Esto fomenta las pruebas más realistas y centradas en el usuario.

Jest y React Testing Library se complementan perfectamente. Jest ofrece las capacidades generales para la ejecución de pruebas, mientras que React Testing Library se enfoca en la interacción y la experiencia del usuario al probar componentes React. Juntas, estas herramientas permiten un enfoque completo para garantizar que una aplicación React funcione correctamente tanto en términos de funcionalidad como de usabilidad.

¿Qué es el Trofeo de test?

El concepto del "Trofeo de Test" se puede aplicar a diferentes niveles de pruebas en el desarrollo de software, desde las pruebas estáticas hasta las pruebas de extremo a extremo. Cada nivel de prueba contribuye a ganar este "trofeo" al asegurar la calidad y la confiabilidad del software en diferentes aspectos. El trofeo de testing es un modelo de pruebas de software que se enfoca en encontrar el equilibrio adecuado entre la inversión de tiempo y el retorno de confianza en la aplicación de software, este modelo se enfoca en escribir suficientes pruebas, pero no necesariamente alcanzar una cobertura del 100%.

Veamos cómo se aplica el "Trofeo de Test" a los diferentes niveles:

Pruebas Estáticas

Las pruebas estáticas son similares a revisar un libro en busca de errores antes de leerlo. Antes de ejecutar el software, se analiza el código en busca de errores y se asegura de que esté bien escrito y documentado. Esto es esencial para garantizar la calidad, confiabilidad y facilidad de mantenimiento del software.

  • En las pruebas estáticas, se evalúa el código sin ejecutarlo para identificar posibles errores de programación.
  • Se revisa que el código cumpla con estándares de codificación y buenas prácticas.
  • Que esté documentado de manera clara para facilitar su comprensión.

Para pruebas estáticas, puedes utilizar herramientas como linters específicos del lenguaje, como ESLint para JavaScript o TSLint para TypeScript, que te ayudarán a encontrar problemas de calidad de código y mantener un estilo de codificación consistente.

Pruebas Unitarias

Estas pruebas se realizan cerca de la fuente de la aplicación y consisten en probar métodos y funciones individuales de las clases, componentes o módulos que usa el software. Las pruebas unitarias son una herramienta útil para aislar y probar unidades específicas de código para determinar la eficacia de cada componente.

En cuanto a las tecnologías que se usan en Typescript son Jest, Mocha, Chai, en cuanto en Java JUnit, Mockito, TestNGy React tenemos, React Testing Library, Jest, Enzyme.

Pruebas de Integración

Las pruebas de integración se centran en verificar cómo interactúan diferentes componentes o módulos de software cuando se combinan. El objetivo principal es asegurarse de que las partes individuales del software funcionen correctamente cuando se integran para formar un sistema más grande.

Ganar el "Trofeo de Test" en las pruebas de integración implica:

  • Identificar componentes clave y definir escenarios de integración.
  • Desarrollar y automatizar casos de prueba específicos para escenarios.
  • Ejecutar pruebas, gestionar problemas y mejorar continuamente las pruebas para una integración efectiva.

En cuanto a las tecnologías que se usan en Typescript son Supertest, Cypress, en cuanto en Java Spring Boot Test, REST Assured, TestContainers y React tenemos, Cypress, Puppeteer.

Pruebas de Extremo a Extremo (End-to-End)

Las pruebas, de extremo a extremo, evalúan el software en su conjunto, como lo haría un usuario final. Se aseguran de que todas las partes del sistema estén integradas y funcionen correctamente desde el principio hasta el final del proceso.

Para poder ganar el trofeo de test se deben considerar las siguientes características:

  • Probar el flujo completo de la aplicación, simulando acciones del usuario.
  • Verificar que todos los componentes se integren correctamente y funcionen como se espera.
  • Se debe comprobar que la aplicación responda de manera adecuada ante diferentes situaciones del mundo real.

Para aplicaciones web, las herramientas como Cypress, Puppeteer o Selenium son ampliamente utilizadas para realizar pruebas de extremo a extremo.

👉Ganar el "Trofeo de Test" en cada nivel aumenta la confianza del equipo en la calidad del software. Las pruebas, incluyendo estáticas, unitarias, de integración y de extremo a extremo, garantizan que el código se mantenga robusto y resistente a desafíos del mundo real.

¿Qué son los Tests Unitarios?

Los tests unitarios son una práctica fundamental en las aplicaciones que estás creando. La idea es probar si una pequeña parte del código funciona bien por sí sola, sin importar cómo se conecta con el resto del código. Esto ayuda a encontrar errores temprano en el proceso de desarrollo y a crear un software más confiable y de mejor calidad.

Características de los test unitarios:

  • Enfoque en la Unidad: Los tests unitarios prueban la parte más pequeña de nuestro código de forma aislada, como una función, método o componente de la interfaz de usuario. Se evalúa cada unidad por separado para asegurarnos de que funcione correctamente en su propio mundo, sin considerar el resto del sistema.
  • Aislamiento y Rapidez: Las pruebas unitarias tienen la ventaja de ejecutarse en un entorno aislado, lo que permite detectar problemas específicos con precisión sin afectar otros elementos del sistema. Además, al concentrarse en unidades pequeñas, son rápidas de ejecutar, lo que proporciona retroalimentación instantánea a los desarrolladores y ahorra tiempo en comparación con pruebas más largas.
  • Cobertura y Mantenibilidad: La 'cobertura de pruebas' se refiere a la proporción de nuestro código que ha sido evaluado por nuestras pruebas. Si bien una mayor cobertura puede brindar una mayor confianza, no siempre garantiza la confiabilidad. Lo que realmente importa es el diseño de las pruebas y la calidad de los componentes que se evalúan. La cobertura por sí sola no es suficiente para garantizar la calidad de las pruebas, tenemos en que el código funciona correctamente. Además, los tests unitarios ayudan a mantener nuestro código con el tiempo. Cuando hacemos cambios en una parte del código, estas pruebas nos dicen si afectan el comportamiento esperado. Esto previene problemas en el futuro y nos da un mayor control sobre cómo evoluciona el código.

¿Por qué hay que hacer pruebas unitarias?

La realización de pruebas es esencial, ya que garantiza que nuestro código funcione de acuerdo a las expectativas y mantenga los estándares de calidad. Las pruebas son un factor crucial para evitar errores en la producción que podrían afectar negativamente la aplicación o disminuir su calidad.

A pesar de las restricciones de tiempo que a veces enfrentamos en la industria tecnológica, las pruebas representan una inversión sumamente valiosa. A largo plazo, simplifican el proceso de mantenimiento y nos aseguran que el código cumpla con nuestros requisitos.

Existen varias razones para esto:

  • Detección Temprana de Errores: Las pruebas evitan la aparición de errores costosos al identificarlos en las etapas iniciales del desarrollo.
  • Facilitación de Cambios Rápidos: Las pruebas ayudan a los desarrolladores a comprender el código y a implementar cambios de manera ágil, especialmente en proyectos recientes.
  • Valiosa Documentación: Las pruebas unitarias en desarrollo de software funcionan como una forma de documentación. Explican el propósito y funcionamiento de cada componente, previenen errores, fomentan la colaboración y reducen el tiempo necesario para familiarizarse con el código. Además, identifican componentes reutilizables y mantienen la documentación actualizada, lo que en última instancia mejora la eficiencia y calidad del proyecto.

Preparación del Entorno

Antes de comenzar, asegúrate de tener Node.js y npm (o yarn) instalados en tu sistema. Luego, crea un nuevo proyecto de React con TypeScript(Opcional, dependerá del proyecto):


npx create-react-app mi-proyecto --template typescript
cd mi-proyecto


Ya creado o en el proyecto que estes trabajado, necesitarás instalar Jest y React Testing Library:

npm install --save-dev jest @testing-library/react @testing-library/jest-dom @types/jes

Crea un archivo jest.config.js en la raíz de tu proyecto. Puedes configurar Jest según tus necesidades. Aquí hay un ejemplo de configuración básica con TypeScript:


const config = {
  verbose: true,
  preset: 'ts-jest',
  testEnvironment: 'node',
  moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
  testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$',
  transform: {
    '^.+\\.(ts|tsx)$': 'ts-jest',
  },
};

export default config;


Hay que tener en cuenta que esta configuración es una configuración básica. Se puede personalizar según las necesidades específicas del proyecto.

Se debe integrarl el package.json la configuración para jest:


"scripts": {
		"test": "jest --watch",
		"test:coverage": "jest --coverage",
	},


"test": "jest --watch": Ejecuta pruebas mientras trabajas y te muestra resultados al instante.

"test:coverage": "jest --coverage": puede ayudarte a identificar qué partes de tu código necesitan más pruebas.

Una vez que ya esté instalado, para poder ejecutar las pruebas de los test utilizamos el siguiente comando, puede ser solo y nos ejecutará todos los test pero también puede ponerse el nombre del archivo que queremos ejecutar para ir probando uno a uno

Jest buscará automáticamente archivos con el patrón *.test.tsx y ejecutará las pruebas definidas en ellos, en este caso es .tsx porque estamos usando typescript.


npx test


Jest buscará automáticamente archivos con el patrón *.test.tsx y ejecutará las pruebas definidas en ellos, en este caso es .tsx porque estamos usando typescript.


npx test  mi-test.test.tsx


Métodos de Comparación en Jest y React Testing Library

Cuando escribimos pruebas para nuestro código en React usando Jest y React Testing Library, es importante saber cómo comparar los resultados esperados con los valores obtenidos durante las pruebas. En este documento, veremos cómo hacer estas comparaciones de manera efectiva.

  • .toBe(): Este método se utiliza para probar la igualdad exacta entre dos valores. Utiliza el algoritmo Object.is para determinar si dos valores son iguales.
  • .toEqual(): A diferencia de .toBe(), este método realiza una comparación recursiva de cada campo en un objeto o array. Es útil cuando se trabaja con estructuras de datos más complejas.

Tipos de queries en Testing library (screen)

  • getBy: La función getBy se utiliza para buscar un elemento en el componente renderizado DOM. Si no se encuentra el elemento que cumple con los criterios, esta función generará un error y la prueba fallará.

const element = screen.getByText('Texto en el elemento');

  • queryBy: La función queryBy también se utiliza para buscar elementos en el DOM. Sin embargo, a diferencia de getBy, si no se encuentra el elemento, en lugar de generar un error, queryBy simplemente devuelve null.

const element = screen.queryByText('Texto en el elemento');

  • findBy: La función findBy se utiliza para buscar un elemento en el DOM, pero con una diferencia importante: puede manejar búsquedas asincrónicas, como cuando se espera que un elemento aparezca después de una acción asíncrona, como una solicitud HTTP.

const element = await screen.findByText('Texto en el elemento');

Principales querys

  1. getByRole: Esta consulta puede usarse para buscar todos los elementos que están expuestos en el árbol de accesibilidad. Con la opción name, puedes filtrar los elementos devueltos por su nombre accesible. Debería ser tu preferencia principal para casi todo.
  2. getByLabelText: Es muy útil para campos de formulario. Los usuarios navegan por formularios utilizando etiquetas. Esta consulta emula ese comportamiento y debería ser tu primera opción para campos de formulario.
  3. getByPlaceholderText: Un marcador de posición no reemplaza a una etiqueta, pero si eso es lo único que tienes, es mejor que otras alternativas.
  4. getByText: Fuera de los formularios, el contenido de texto es la principal manera en que los usuarios encuentran elementos. Puede usarse para encontrar elementos no interactivos como divs, spans y párrafos.
  5. getByDisplayValue: El valor actual de un elemento de formulario puede ser útil al navegar por una página con valores completados.

Semantic Queries

  1. getByAltText: Si el elemento es uno que admite texto alternativo (img, área, input y cualquier elemento personalizado), se puede usar esto para encontrar ese elemento.
  2. getByTitle: El atributo title no se lee de manera consistente por los lectores de pantalla y no es visible de manera predeterminada para los usuarios con vista.

IDs de Prueba

  1. getByTestId: El usuario no puede verlos (ni oírlos), por lo que solo se recomienda en casos donde no puedas hacer coincidir por rol o texto, o no tiene sentido (por ejemplo, el texto es dinámico).
💡Para comprender mejor estos conceptos, se recomienda revisar la documentación oficial y ver un video que, en mi opinión, resultó muy útil para entender las pruebas unitarias. https://testing-library.com/docs/queries/about/

¿Como saber cuando un test es async?

En JavaScript, y en el contexto de pruebas unitarias, como Jest y React Testing Library, un test se considera asíncrono (async) cuando involucra operaciones que no se ejecutan de inmediato, como llamadas a funciones que devuelven Promesas, temporizadores (por ejemplo, setTimeout) o interacciones con APIs asíncronas (como solicitudes de red).

Test Sincrónico (No-Async):

  • No utiliza funciones async o await.
  • No realiza llamadas asincrónicas a APIs, servicios o bases de datos.
  • No utiliza setTimeout o setInterval.

test('Renderiza Container correctamente', () => {
	render();
	const userList = screen.getByTestId('user-list');
	expect(userList).toBeInTheDocument();
});

Test Asincrónico (Async):

  • Utiliza la palabra clave async antes de la función de prueba (async () => {...}).
  • Utiliza await al llamar a funciones asincrónicas o esperar promesas.
  • Involucra llamadas a funciones que devuelven promesas.

test('Renderiza UserListContainer correctamente de manera asincrónica', async () => {

  render();

  await waitFor(() => {
    const userList = screen.queryByTestId('user-list');
    expect(userList).toBeInTheDocument();
  }, { timeout: 1000 });
});

Creación de Pruebas

  • Ejemplo 1: Supongamos que tenemos un componente llamado Button que se desea probar. Primero, hay que crear un archivo llamado Button.test.js. Aquí hay un ejemplo de cómo podría ser una prueba simple para el componente Button:

import React from 'react';
import { render, screen } from '@testing-library/react';
import Button from './Button';

test('renders button with correct text', () => {
  render(

En este ejemplo, estamos utilizando las funciones render y screen.getByText de React Testing Library para renderizar el componente Button y verificar si el elemento de botón con el texto "Click me" está presente en el documento.

  • Ejemplo 2: Como inicio de este test se está creando un objeto handlers que contiene varias funciones simuladas utilizando jest.fn(). Se usan funciones simuladas para imitar lo que sucede cuando se llama a un handler de eventos dentro del componente que se está probando. Cada función representa una acción específica que se espera que el componente realice.

💡 jest.fn(): Es una herramienta que permite crear funciones simuladas que guardan información sobre sus llamadas durante las pruebas. Esto es útil para verificar si una función se llama correctamente o para imitar el comportamiento de funciones en situaciones de prueba.


const handlers = {
	onChange: jest.fn(),
	onSearchContainer: jest.fn(),
	onClear: jest.fn(),
	onCheckpointChange: jest.fn()
};


Supongamos que debemos testear si hay o no un titulo en el componente EventExceptionForm se puede iniciar la función del test con test o it.


test('renders the title correctly and checks if handlers are called', () => {
 
  render();

  expect(screen.getByText('Evento/Excepción')).toBeInTheDocument();
  fireEvent.click(screen.getByText('Botón que llama al handler'));

  // Verifica que los controladores se hayan llamado
  expect(handlers.onChange).toHaveBeenCalled();
  expect(handlers.onSearchContainer).toHaveBeenCalled();
  expect(handlers.onClear).toHaveBeenCalled();
  expect(handlers.onCheckpointChange).toHaveBeenCalled();
});


  • Renderiza el componente EventExceptionForm con una lista vacía de checkpoints y los handlers proporcionados. El uso de ... en este caso es para pasar todas las propiedades del objeto handlers al componente EventExceptionForm en JavaScript. Si el objeto contiene funciones que manejan eventos, estas funciones se pasarán al componente para que pueda interactuar con ellas.
  • Busca un elemento en la pantalla que contenga el texto 'Evento/Excepción'.
  • expect(title).toBeInTheDocument(); utiliza Jest para verificar si el elemento del título se encuentra en el documento (es decir, si se ha renderizado correctamente). Si no se encuentra, el test fallará.

Luego, se utilizan múltiples afirmaciones expect para verificar que los controladores se hayan llamado. Esto se hace con las siguientes líneas:

  • expect(handlers.onChange).toHaveBeenCalled(): Verifica si el controlador onChange se ha llamado al menos una vez.
  • expect(handlers.onSearchContainer).toHaveBeenCalled(): Verifica si el controlador onSearchContainer se ha llamado al menos una vez.
  • expect(handlers.onClear).toHaveBeenCalled(): Verifica si el controlador onClear se ha llamado al menos una vez.
  • expect(handlers.onCheckpointChange).toHaveBeenCalled(): Verifica si el controlador onCheckpointChange se ha llamado al menos una vez.

  • Ejemplo 3:

it('The button renders and calls the clearForm function on click', () => {
  const clearForm = jest.fn();

  render(
    
  );

  const buttonElement = screen.getByRole('button', { name: 'Nuevo proceso de interpinchazo' });
  expect(buttonElement).toBeInTheDocument();
  expect(buttonElement).not.toBeDisabled();

  fireEvent.click(buttonElement);
  expect(clearForm).toHaveBeenCalledTimes(1);
});


  • Se crea una función simulada clearForm utilizando jest.fn(). Esta función simulada se utilizará como el handler de evento onClick del botón.
  • Se renderiza el componente Button , en este caso, se le pasa el texto "Nuevo proceso de interpinchazo", se establece el tipo como "button", y se asigna clearForm como el handler de evento onClick.
  • Se utiliza screen.getByRole para buscar un elemento de botón que tenga el nombre "Nuevo proceso de interpinchazo". Luego se verifica que el botón esté presente en el documento y no esté deshabilitado.
  • fireEvent.click(buttonElement) simula un clic en el botón.
  • Finalmente, se verifica que la función clearForm haya sido llamada exactamente una vez (toHaveBeenCalledTimes(1)) después de hacer clic en el botón.

  • Ejemplo 4: Se abre el modal que contiene este componente, y tiene una condición dependiendo del estado en el que se encuentra. El componente que vamos a probar es el Item. Es un modal que recibe la propiedad de container, la cual tendremos un contenedor mockeado para poder realizar las pruebas correspondientes. Verificaremos que según el estado (currentState) en el que se encuentre el contenedor, se mostrará un botón con un texto diferente.


const container = {
    codeContainer: '7654152698',
    currentState: 'created',
}

it('Render the correct button based on the state', () => {
    const { getByText } = render();

 const buttonText =
  container.currentState === 'sealed'
    ? 'Auditar Contenedor'
    : container.currentState === 'onaudit'
    ? 'Cambiar estado'
    : container.currentState === 'disabled'
    ? 'Ir al Contenedor'
    : 'Cargar Contenedor';

    const button = getByText(buttonText);

    expect(button).toBeInTheDocument();
  });


La expresión utiliza múltiples operadores ternarios (? :) para determinar el valor de **buttonText** basado en el estado del contenedor (container.currentState).

Este código realiza una prueba para confirmar que el botón mostrado en la interfaz de usuario coincida con el estado actual de un contenedor. Asegura que la interfaz muestre el botón adecuado para el estado del contenedor.

Conclusión

Las pruebas unitarias son una parte esencial del proceso de desarrollo de software. Nos permiten identificar y solucionar problemas en nuestros componentes y funciones antes de que lleguen a producción. Al utilizar Jest y React Testing Library, puedo crear pruebas unitarias efectivas para mis aplicaciones React. Entiendo que escribir pruebas sólidas no solo mejora la calidad de mi código, sino que también facilita el mantenimiento y la evolución de mi proyecto.

Referencias

https://medium.com/@angelygranados/cómo-empezar-a-hacer-unit-testing-con-jest-guía-básica-ca6d9654672

https://www.youtube.com/watch?v=QdqIqGPsLW0

https://www.youtube.com/watch?v=bTGil8qPmXo

Karla Cabañas

October 24, 2023

Entradas anteriores