Saltar al contenido principal

Pruebas unitarias

Para cualquier proyecto de JDP, la funcionalidad de pruebas unitarias está integrada. Debería ver que @jutro/test está presente en el package.json de la sección devDependencies.

Después de que el proyecto haya ejecutado npm install o npm ci, puede verificar la funcionalidad de pruebas unitarias mediante la ejecución del comando npm run test en la línea de comando. Se prevé que vea que los casos de prueba se han ejecutado, cualquier caso de falla de prueba y un informe de la cobertura de la prueba al final.

Tenga en cuenta que es posible ver algunos errores TypeScript en algunos métodos de prueba, por ejemplo toBeInTheDocument(), después de la generación.

Además, muchos de los métodos de aserción de la biblioteca de pruebas no están disponibles con la configuración tal como se proporcionan (métodos relacionados con Jest Dom).

Las aplicaciones de Jutro ejecutan Jest a través de la interfaz react-app-rewired.

Ejecución de pruebas unitarias​

Jest ejecuta nuestras pruebas de aplicaciones, por eso, tener a mano la documentación sobre las opciones de CLI de Jest puede ser muy útil.

Puede ejecutar todas las pruebas unitarias en su aplicación desde la raíz del proyecto con el comando. En la raíz de la aplicación, ejecute:

npm run test

Si echamos un vistazo a este comando en package.json, veremos que está usando la CLI de Jutro. Por lo tanto, también podemos ejecutar todas las pruebas unitarias en la aplicación de esta manera:

jutro app:tests

La principal diferencia entre los dos anteriores es el indicador --collectCoverage. Podemos usar indicadores relacionados con Jest para este comando directamente.

Encontrará más información sobre el uso de la CLI de Jutro para las pruebas aquí.

El indicador --collectCoverage es un alias de --coverage[=<boolean>], lo que indica que la información de cobertura de las pruebas se debe recopilar y notificar en la salida. Con este indicador, verá un informe de cobertura de prueba.

Este indicador también genera un nuevo informe de cobertura que se puede ver al abrir coverage\lcov-report\index.html.

Ejecución de pruebas para un archivo específico​

Es posible que desee ejecutar pruebas para un archivo o componente específico, a fin de acelerar el proceso de prueba.

Puede ejecutar una sola prueba como esta:

npm run test <TestFileName>
----
jutro app:tests <TestFileName>
----
jutro app:tests path/to/<TestFileName>.test.tsx

Recuerde que npm run test <TestFileName> usará el método --collectCoverage..

Si desea ejecutar más de una prueba, simplemente agregue tantos nombres de archivo de prueba como sea necesario a los comandos anteriores.

También puede ejecutar pruebas para un archivo específico en Jest, usando la opción --findRelatedTests seguida de la ruta al archivo de destino.

npm test -- --findRelatedTests path/to/your-test-file.test.js

Por ejemplo, si tiene un archivo de prueba ubicado en src/js/components/Tools.test.tsx, puede ejecutar las pruebas en ese archivo con el siguiente comando:

npm test -- --findRelatedTests src/js/components/Tools.test.tsx

Convenciones de archivos​

Agregue pruebas a componentes y páginas según sea necesario. Vea los ejemplos en:

  • src/app/__tests__
  • src/Pages/CodelessForm/__tests__

Ejemplo de pruebas unitarias de componentes

Existen algunos casos de prueba comunes que se pueden utilizar para probar un componente, por ejemplo:

  1. El elemento de prueba existe/no existe:
expect(screen.getByRole('button')).toBeInTheDocument();
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
  1. Se coincide con el contenido de la prueba:
expect(screen.getByRole('heading').textContent).toBe('Hello World');
expect(screen.getByRole('buttonOne').textContent.toBe(getTranslation(messages.buttonTitle));
  1. El valor de la prueba es verdadero/falso:
expect(screen.getByDisplayValue('flag').nodeValue).toBeTrue
expect(screen.getByDisplayValue('flag').nodeValue).toBeFalse
  1. Probar si se ha llamado a la función x veces:
const onChange= jest.fn();
render(<MyDropdown onChange={onChange} />);
userEvent.click(dropdown);
expect(onChange).toHaveBeenCalledTimes(1);

Configuración adicional para pruebas unitarias​

Jest tiene una configuración predefinida para las aplicaciones de Jutro. Si desea agregar sus propias opciones de configuración, primero debe crear un archivo llamado jest.config.js. Luego, siga cualquiera de los dos métodos a continuación:

  • Exportar un objeto. Con este método, su configuración se combina con la configuración básica.
jest.config.js
module.exports = {
setupFilesAfterEnv: [
require.resolve('src/setupTests.js'), // path to extra setup file.
],
};
  • Exportar una función, en la que la configuración base sea el primer argumento.
jest.config.js
module.exports = (basicConfig) => {
const newConf = { ...basicConfig };
newConf.setupFilesAfterEnv.push(require.resolve('src/setupTests.js'));
return newConf;
};

Ayudantes en pruebas unitarias​

El paquete @jutro/test de Jutro proporciona una serie de funciones de ayuda que pueden ser útiles para ejecutar pruebas, especialmente en componentes personalizados.

Consulte Ayudantes en las pruebas de Jutro.

Dependencias simuladas​

Las funciones simuladas le permiten probar los enlaces entre el código mediante la eliminación de la implementación real de una función, la captura de llamadas a la función (y los parámetros pasados en esas llamadas) y la configuración en tiempo de prueba de los valores de retorno.

Además, en algunos escenarios, deberá simular las dependencias para evitar comportamientos inesperados. Por ejemplo, la prueba unitaria se quedará sin memoria cuando se llame a la función schema getter desde el SDK de Digital de Jutro. Para evitar la pérdida de memoria, simulamos la función schema getter.

Hay dos maneras de simular funciones, que son las siguientes:

  • Crear una función simulada para usarla en el código de prueba.
  • Escribir una simulación manual para invalidar una dependencia de módulo.

Simulación de llamadas a la API​

Para simular llamadas a la API, puede usar el paquete jest.mock. Para obtener más información sobre la simulación con Jest, consulte Documentación de Jest sobre funciones simuladas.

Supongamos que su componente muestra un avatar de usuario utilizando el paquete de transporte de Jutro.

UserAvatar.js
import React, { useEffect, useState } from 'react';
import { createHttpRequest } from '@jutro/transport';

const baseUrl = 'https://jsonplaceholder.typicode.com';

const getUserData = () => {
return createHttpRequest(baseUrl, true).get('/users/1', {});
};

export const UserAvatar = () => {
const [name, setName] = useState();

useEffect(() => {
getUserData().then((userData) => {
setName(userData.name);
});
}, []);

return (
<div
style={{
fontSize: '2rem',
padding: '3rem',
display: 'flex',
gap: '1rem',
flexDirection: 'column',
}}>
<div>{name}</div>
</div>
);
};

Para escribir una prueba para este componente, simule la funciónget() desde createHttpRequest:

UserAvatar.test.js
import React from 'react';
import { render, screen } from '@jutro/test';
import { UserAvatar } from '../UserAvatar';

const explorerName = 'Marco Polo';
jest.mock('@jutro/transport', () => ({
...jest.requireActual('@jutro/transport'),
createHttpRequest: () => ({
get: async () => ({
name: explorerName,
}),
}),
}));

describe('UserAvatar', () => {
it('Displays the name of the famous explorer', async () => {
render(<UserAvatar />);
expect(await screen.findByText(explorerName)).toBeInTheDocument();
});
});

Simulación de configuración​

Para probar diferentes valores de configuración, puede usar jest.mock. Por ejemplo, imagine un componente llamado ConfigReader que muestra valores de la configuración de Jutro:

ConfigReader.js
import React from 'react';
import { useTranslator } from '@jutro/locale';
import { getConfigValue } from '@jutro/config';

import messages from './ConfigReader.messages';

export const ConfigReader = () => {
const translator = useTranslator();

const configValue = getConfigValue('SOME_CONFIG', '<default value>');

return (
<div data-testid="config-display">
{translator(messages.displayConfigValue, { configValue })}
</div>
);
};

Puede probarlo con el siguiente código:

ConfigReader.test.js
import React from 'react';
import { render, screen, getTranslation } from '@jutro/test';
import { getConfigValue } from '@jutro/config';

import { ConfigReader } from '../ConfigReader';

// Before running tests in this file we need to mock the `getConfigValue` function.
// This particular function requires a bit more handling, as it is used during setup
// and needs a basic implementation.
jest.mock('@jutro/config', () => ({
...jest.requireActual('@jutro/config'),
getConfigValue: jest.fn((_path, defaultValue) => defaultValue),
}));

describe('handling env variables', () => {
describe('using default values', () => {
beforeEach(() => {
getConfigValue.mockImplementation((_path, defaultValue) => defaultValue);
});

it('renders default config value', () => {
render(<ConfigReader />);

expect(
screen.getByText(getTranslation('Config value is: <default value>'))
).toBeInTheDocument();
});
});

describe('using mocked values', () => {
const mockedEnvVariables = {
SOME_CONFIG: '<mocked env var>',
};

beforeEach(() => {
getConfigValue.mockImplementation(
(path, defaultValue) => mockedEnvVariables[path] ?? defaultValue
);
});

it('renders mocked config value', () => {
render(<ConfigReader />);

expect(
screen.getByText(getTranslation('Config value is: <mocked env var>'))
).toBeInTheDocument();
});
});
});

Simulación global de configuración​

Puede simular la configuración globalmente para todas las pruebas de su aplicación mediante la adición de un archivo src/__mocks__/@jutro/config.js.

const actual = jest.requireActual('@jutro/config');

module.exports = {
...actual,
getConfigValue: (path, defaultValue) => {
if (
path === 'accessibleDisabled.all' ||
path === 'accessibleDisabled.button'
) {
return true;
}

return actual.getConfigValue(path, defaultValue);
},
};

Pruebas a componentes que aparecen o desaparecen​

Para probar si algo aparece o desaparece después de una interacción específica, use un modal de Jutro:

MyModalExample.js
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { useTranslator } from '@jutro/locale';
import { ModalNext } from '@jutro/components';

import { messages } from './MyModalExample.messages';
import { OpenModalTrigger } from './OpenModalTrigger';

export const MyModalExample = () => {
const translator = useTranslator();

const [showModal, setShowModal] = useState(false);

return (
<React.Fragment>
<OpenModalTrigger
onClick={() => setShowModal((value) => !value)}
isOpen={showModal}
/>
<ModalNext isOpen={showModal}>
<span>{translator(messages.modalContent)}</span>
</ModalNext>
</React.Fragment>
);
};

MyModalExample.propTypes = {
title: PropTypes.string,
};

MyModalExample.defaultProps = {
title: 'sample',
};
OpenModalTrigger.js
import React from 'react';
import PropTypes from 'prop-types';
import { Button } from '@jutro/components';

import { messages } from '../MyModalExample.messages';

export const OpenModalTrigger = ({ onClick, isOpen }) => (
<Button
data-testid="youDontNeedThisId"
onClick={onClick}>
{isOpen ? messages.hideModalButton : messages.showModalButton}
</Button>
);

OpenModalTrigger.propTypes = {
onClick: PropTypes.func,
isOpen: PropTypes.bool,
};

Puede probar el modal con el siguiente código. Tenga en cuenta los comentarios en el código que explican las pruebas específicas con mayor detalle.

MyModalExample.test.js
import React from 'react';
import { render, screen, getTranslation, within } from '@jutro/test';
import userEvent from '@testing-library/user-event';

import { messages } from '../MyModalExample.messages';
import { MyModalExample } from '../MyModalExample';

describe('Testing something appears/disappears', () => {
it('does not show modal by default', () => {
render(<MyModalExample />);

expect(
screen.queryByText(getTranslation(messages.modalContent))
).not.toBeInTheDocument();
});

it('toggles modal after clicking the button', async () => {
render(<MyModalExample />);

// When testing for an element not being present on a page,
// be the least specific you can.
// Otherwise it's easy to write a test that doesn't work properly.
// Imagine you want to test that there are no modals on the page, so you wrote a test:
// "I expect there is no modal with title 'My modal' that is also blue."
// "Correct" (but you won't know that there is a modal, but red).
// If you had instead:
// "I expect there is no modal."
// The answer would be "Incorrect, there is a red modal" and you know to fix the component or test.
// In testing if something exists it's the opposite: provide as many details as it makes sense.
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();

await userEvent.click(
screen.getByRole('button', {
name: getTranslation(messages.showModalButton),
})
);

expect(screen.getByRole('dialog')).toBeInTheDocument();

// Another thing:
// If you are testing for an element not being present,
// also include an opposite assertion with the same query.
// You might make a mistake and look for an element that could never exist,
// so the test will always pass.
// It's not enough to do:
// "I expect there is no 'thingumajig' on the page."
// "Correct, there is no 'thingumajig'."
// Besides that, you also need an opposite assertion:
// <try to enable the thing>
// "This time I expect there is 'thingumajig' on the page."
// "Incorrect, there is still no 'thingumajig'."
// This is how you know the element you are toggling is actually 'thingummy',
// and the test will catch when someone changes 'thingummy' to 'thingumabob' in the future.
userEvent.click(
screen.getByRole('button', {
name: getTranslation(messages.hideModalButton),
})
);

expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
});

it('modal contains the expected content', () => {
render(<MyModalExample />);

userEvent.click(
screen.getByRole('button', {
name: getTranslation(messages.showModalButton),
})
);

// if you have multiple similar elements found by the same query, `within` can help with that.
// Don't overuse within if you don't need to
expect(
within(screen.getByRole('dialog')).getByText(
getTranslation(messages.modalContent)
)
).toBeInTheDocument();
});
});

Selección de una opción de un menú desplegable​

Para probar la selección de un DropdownSelectField, creemos el siguiente componente:

MyDropdown.js
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { DropdownSelectField } from '@jutro/legacy/components';

import styles from './MyDropdown.module.scss';
import { messages } from './MyDropdown.messages';

const availableValues = [
{
code: 'option-1',
name: { id: 'option-1.name', defaultMessage: 'Option 1' },
},
{
code: 'option-2',
name: { id: 'option-2.name', defaultMessage: 'Option 2' },
},
{
code: 'option-3',
name: { id: 'option-another.name', defaultMessage: 'Another option' },
},
];

export const MyDropdown = ({ onValueChange }) => {
const [selectedValue, setSelectedValue] = useState(null);

return (
<DropdownSelectField
id="test-dropdown"
label={messages.label}
value={selectedValue}
onValueChange={(newValue) => {
setSelectedValue(newValue);
onValueChange?.(newValue);
}}
availableValues={availableValues}
/>
);
};

MyDropdown.propTypes = {
onValueChange: PropTypes.func,
};

Puede usar lo siguiente como modelo para sus pruebas desplegables.

MyDropdown.test.js
import React from 'react';
import {
render,
screen,
getTranslation,
checkA11yViolations,
userEvent,
} from '@testing-library/user-event';

import { messages } from '../MyDropdown.messages';
import { MyDropdown } from '../MyDropdown';

describe('Testing a dropdown', () => {
it('exists', () => {
render(<MyDropdown />);

screen.getByRole('combobox', { name: getTranslation(messages.label) });
});

it('contains all expected options', () => {
const expectedOptions = [
getTranslation('Option 1'),
getTranslation('Option 2'),
getTranslation('Another option'),
];

render(<MyDropdown />);

const dropdown = screen.getByRole('combobox', {
name: getTranslation(messages.label),
});

userEvent.click(dropdown);

expectedOptions.forEach((expectedOption) => {
expect(
screen.getByRole('option', { name: expectedOption })
).toBeInTheDocument();
});
});

it('updates the value when selected', () => {
const onChange = jest.fn();

render(<MyDropdown onChange={onChange} />);

const dropdown = screen.getByRole('combobox', {
name: getTranslation(messages.label),
});

userEvent.click(dropdown);

const firstOption = screen.getByRole('option', {
name: getTranslation('Option 1'),
});
const secondOption = screen.getByRole('option', {
name: getTranslation('Option 2'),
});

userEvent.click(firstOption);

// assert
expect(onChange).toHaveBeenCalledTimes(1);
expect(onChange).toHaveBeenCalledWith('option-1');
expect(dropdown).toHaveValue('option-1');

userEvent.click(dropdown);
userEvent.click(secondOption);

// assert
expect(onChange).toHaveBeenCalledTimes(2);
expect(onChange).toHaveBeenCalledWith('option-2');
expect(dropdown).toHaveValue('option-2');
});
});

Introducción de datos en una DateInput​

Para probar DateInput, comencemos con el siguiente componente:

MyDateComponent.js
import React, { useCallback, useState } from 'react';
import { DateInput } from '@jutro/components';
import { useTranslator } from '@jutro/locale';

import messages from './MyDateComponent.messages';

export const MyDateComponent = ({ onValueChange: onValueChangeProp }) => {
const translator = useTranslator();

const [value, setValue] = useState();

const onValueChange = useCallback(
(newValue) => {
setValue(newValue);
onValueChangeProp(newValue);
},
[onValueChangeProp]
);

return (
<DateInput
id="myDate"
value={value}
onValueChange={onValueChange}
label={messages.dateLabel}
/>
);
};

Puede utilizar las siguientes pruebas como modelo:

MyDateComponent.test.js
import React from 'react';
import { render, screen, getTranslation } from '@jutro/test';
import userEvent from '@testing-library/user-event';

import { MyDateComponent } from '../MyDateComponent';

const selectDate = async (date) => {
const dateInput = screen.getByRole('textbox', {
name: getTranslation('Select your date'),
});

await userEvent.type(dateInput, date);
};

const selectTime = async (time) => {
const timeInput = screen.getByRole('textbox', {
name: /Select your time/,
});

// await userEvent.type(timeInput, time);

await userEvent.click(timeInput);

const timeOption = screen.getByRole('option', {
name: time,
});

await userEvent.click(timeOption);
};

const selectTimezone = async (timezone) => {
const timezoneInput = screen.getByRole('combobox', {
name: getTranslation('Select your timezone'),
});

await userEvent.click(timezoneInput);

const timezoneOption = screen.getByRole('option', {
name: getTranslation(timezone),
});

await userEvent.click(timezoneOption);
};

describe('MyDateComponent', () => {
it('updates date, time and timezone', async () => {
const onValueChange = jest.fn();

render(<MyDateComponent onValueChange={onValueChange} />);

await selectDate('10/30/1994');
await selectTime('06:30 PM');
await selectTimezone('Antarctica/Troll');

expect(onValueChange).toHaveBeenCalledWith({
datetime: { year: 1994, month: 9, day: 30, hour: 18, minute: 30 },
timezone: 'Antarctica/Troll',
});
});
});

Puntos de prueba clave que se deben considerar​

  • Trate de evitar obtener el elemento por su ID: Los ID se pueden cambiar fácilmente y, a veces, se generan automáticamente, así que obtenga el elemento en particular por getByRole, queryByRole o incluso getByText y getByDisplayValue en su lugar.

  • Pruebe el resultado, no la implementación: Algunas funciones tienen lógicas muy complejas en su interior y pueden tener varias dependencias. Por lo tanto, en lugar de probar la lógica de la función, concéntrese en la entrada y la salida.

  • Use las claves de traducción para probar si el contenido coincide con la clave de visualización: La traducción puede cambiar, así que cuando pruebe las claves de visualización, intente no codificar el texto de forma rígida. Utilice la función getTranslation proporcionada por la biblioteca de pruebas de Jutro para obtener la clave de visualización en su lugar.

Modo estricto de React​

El modo estricto de React es un componente nativo de React que ejecuta verificaciones adicionales cuando está habilitado en un entorno de desarrollo, lo que le ayuda a encontrar errores comunes en sus componentes. Ofrece los siguientes beneficios:

  • Comprueba los componentes para detectar cualquier uso de API obsoletas.
  • Vuelve a renderizar los componentes una vez más para intentar encontrar cualquier componente que cambie involuntariamente su comportamiento cuando se renderiza por segunda vez.
  • Vuelve a ejecutar un ciclo adicional de configuración y limpieza para detectar cualquier efecto en el código.

Para habilitar el modo estricto de React, en su archivo .env, agregue JUTRO_REACT_STRICT_MODE=true.

Las aplicaciones preexistentes no tendrán habilitado el modo estricto de forma predeterminada, sin embargo, cualquier aplicación nueva de Jutro creada a partir de una plantilla de aplicación de Jutro tendrá la variable de entorno habilitada en el archivo .env.

Cuando JUTRO_REACT_STRICT_MODE=true se establece en el archivo .env, el comando jutro app:tests lo lee y se pasa al ayudante de Jutro render dentro de @jutro/tests. Luego, en render, se ejecuta la siguiente verificación:

const isInReactStrictMode = process.env.JUTRO_REACT_STRICT_MODE === 'true';

configure({
reactStrictMode: isInReactStrictMode,
});
Note: Si JUTRO_REACT_STRICT_MODE=true está configurado en el archivo .env, también puede ejecutar el comando npm run test en la línea de comandos para ejecutar la aplicación en modo estricto.