Unit Testing Frontend¶
Pre-requisitos¶
Implementaciones¶
Con respecto al frontend hay distintas herramientas, la más estandarizada y preconfigurada es jest con react-testing-library. Una diferencia importante es que jest se ejecuta en node, por lo que no existe DOM cómo lo conocemos en el browser y es común que ciertas configuraciones y ciertas cosas que damos por hecho en el navegador tengamos que configurarlo en nuestro proyecto (Por ejemplo: ciertos eventos como scroll, mediaqueries, etc)
Las implementaciones varían con respecto a NextJs y CRA
CRA¶
Con respecto al template básico de CRA, tiene por default todo configurado con ciertas limitaciones y defaults:
-
Todos los tests accesibles tienen que estar dentro de la carpeta src que pueden ser de la siguiente manera:
__tests__
,*.spec.tsx
,*.test.tsx
,*.spec.jsx
,*.test.jsx
,*.spec.ts
y/o*.spec.js
.
-
Por default la carpeta
__mocks__
también debe ser incluida en src y puede simular y o falsear comportamiento de librerías siguiendo estructura por ejemplo:@org/library
->__mocks__/@org/library.js
para modificar o cambiar cómo jest comprende estas librerías. -
Por default comprende y soporta por default los imports de imagenes o svg cómo lo hace cra por debajo.
-
Las configuraciones de jest de las que son accesibles son limitadas y se deben hacer mediante el
package.json
en la key:jest
. Ver más en la documentación oficial
Las configuraciones habilitadas son:
- clearMocks
- collectCoverageFrom
- coveragePathIgnorePatterns
- coverageReporters
- coverageThreshold
- displayName
- extraGlobals
- globalSetup
- globalTeardown
- moduleNameMapper
- resetMocks
- resetModules
- restoreMocks
- snapshotSerializers
- testMatch
- transform
- transformIgnorePatterns
- watchPathIgnorePatterns
Por lo que no se puede modificar ni la ruta de dónde se encuentran, las carpetas __tests__
ni __mocks__
.
Las configuraciones importantes son coverageThreshold que permite colocar un minímo para el coverage (por ejemplo mínimo de 5% de lineas cubiertas) y collectCoverageFrom que permite definir sobre que carpetas queremos recuperar el coverage (especialmente util si utilizamos tambien testing e2e).
Configuración¶
Para configurar cualquier variable global por ejemplo podemos hacerlo dentro del archivo setupTest
dentro de la carpeta src
.
En este archivo se suelen colocar todas las configuraciones y mocks habituales para simular el browser en node ( recordemos que Jest corre en un entorno Node, por lo tanto muchas de las funciones de window no existen por ejemplo localStorage
)
Ejemplo de mock de localStorage
const localStorageMock = { getItem: jest.fn(), setItem: jest.fn(), removeItem: jest.fn(), clear: jest.fn(), }; global.localStorage = localStorageMock;
Providers¶
En las aplicaciones se suelen utilizar contextos o Providers puntuales para proveer de cierta informacion a un arbol de componentes puntuales, el caso mas estandar es el Provider
de redux
, el cual provee el estado global a la mayoria de las aplicaciones.
Ejemplo
// omitido por brevedad
ReactDOM.render(
<StyleSystemProvider>
<Provider store={store}>
<App />
</Provider>
</StyleSystemProvider>,
document.getElementById("root")
);
Para garantizar que los componentes funcionen como estan integrados, que por lo general llaman al dispatch
debemos garantizar que nuestros componentes en testing tengan los providers y el contexto necesario. Por lo tanto debemos configurar un render con nuestros providers custom.
Ejemplo de render Custom para Redux
import React from "react";
import { render } from "@testing-library/react";
import type { RenderOptions } from "@testing-library/react";
import { BrowserRouter } from "react-router-dom";
import { Provider } from "react-redux";
import { reducers, RootState, store as defaultStore } from "app/store";
import { StyleSystemProvider } from "@architecture-it/stylesystem";
import { configureStore, PreloadedState } from "@reduxjs/toolkit";
import Alert from "components/Alert";
// Aca se extendien las propiedades del render de testing-library, asi como tambien permitirle al usuario especificar otras propiedades como el estado inicial o el store en caso que se necesite.
interface ExtendedRenderOptions extends Omit<RenderOptions, "queries"> {
preloadedState?: PreloadedState<RootState>;
store?: typeof defaultStore;
}
export default function renderWithProviders(
ui: React.ReactElement,
{
preloadedState = {},
// Automaticamente crea una instancia del store sino es pasado como opción.
store = configureStore({ reducer: reducers, preloadedState }),
...renderOptions
}: ExtendedRenderOptions = {}
) {
function Wrapper({ children }: React.PropsWithChildren<{}>): JSX.Element {
return (
<BrowserRouter>
<StyleSystemProvider>
<Provider store={store}>
{children}
<Alert />
</Provider>
</StyleSystemProvider>
</BrowserRouter>
);
}
// Retorna un objeto con el store con todas las propiedades del render de react testing library
return { store, ...render(ui, { wrapper: Wrapper, ...renderOptions }) };
}
Jest provee métodos y funciones que permiten definir los test, el primer argumento es a modo descriptivo y va a aparecer en la consola, hay dos funciones elementales, el contexto que son conocidos como Test Suites
, por lo general comprenden el test de un componente en React o una funcion por ejemplo:
//Test Suite
describe("<Header />", () =>{
//aca irian los test unitarios
});
// o
describe("utilityFn()", ()=>{
});
Y los test unitarios que pueden ser encontrados cómo test
o it
//Test Suite
describe("<Header />", () =>{
it("Should render without crash",() => {
// Test unitario
});
//o
test("Should render without crash",() => {
// Test unitario
});
});
Sugerimos que el contexto sea el nombre del componente o de la función por ejemplo <Header />
o extractNumbers()
.
Por otra parte la descripción del test unitario debe ser más descriptivo, por lo general se recomienda utilizar debería
o Should
o su respectiva negación.
Primer test¶
Al ser test unitarios deben tener una responsabilidad, el test más básico que recomendamos hacer es un test que viene a ser casi que de regresión. Snapshot.
Los Snapshots vienen siendo como capturas del html string renderizado.
La manera más básica de hacer snapshots es de la siguiente forma, (se genera automaticamente y el mismo debería subirse al repo):
import { render } from "@testing-library/react";
import Header from "@/components/Header"
//Test Suite
describe("<Header />", () =>{
it("Should render without crash",() => {
// asFragment es un método del objeto que devuelve render
// contiene sólo el html string referido al componente
const { asFragment } = render(<Header />);
// Esto genera un archivo de texto con el html, comprueba que en la siguiente ejecución coincida
expect(asFragment()).toMatchSnapshot();
});
});
// snapshot generado
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<Header /> Should render without crash 1`] = `
<DocumentFragment>
<header
class="header"
>
Ejemplo
</header>
</DocumentFragment>
`;
Test Componente presentacional¶
Por lo general son componentes que muestran lo que vendría ser nuestro viewModel
, información de como la necesitamos mostrar y nada más, por lo general hay adapters
previamente.
Supongamos el siguiente componente
import PropTypes from "prop-types"
export default function Header({ title }){
return (
<div>{title}</div>;
)
};
Header.propTypes = {
title: PropTypes.string.isRequired,
};
import { render } from "@testing-library/react";
import Header from "@/components/Header"
//Test Suite
describe("<Header />", () =>{
//snapshot test
it("Should show title by Props",() => {
//Arrange
const title = "Ejemplo"
const { getByText } = render(<Header title={title} />);
//Act
const nodeWithText = getByText();
// Assert
expect(nodeWithText).toBeInTheDocument();
});
});
En la función anterior podemos detectar las 3 partes de la regla de la triple A
- Arrange: construimos el escenario, renderizamos y sacamos el método para buscar el nodo que contiene el texto
- Act: ejecutamos el método que busca el nodo que contiene el texto.
- Assert: Validamos que el nodo exita en el documento.
Correr Tests
Para correr nuestro test en cra plano debemos ejecutar npm run test
esto por default va incluir tests en archivos modificados, no va a ejecutar todos ni va a emplear coverage.
Avanzado¶
Falsos positivos¶
Tenemos que tener cuidado en métodos que son asincronos porque para jest, en un test unitario si no rompe ejecuto correctamente por ejemplo el siguiente código va a dar un falso positivo.
//Header.test.js
import App from "./App"
import { render, screen, expect } from "@testing-library/react";
describe("<Header />", () =>{
it("Should show title by Props",() => {
act(async() => {
render(<App/>);
screen.expect(await screen.findByText("Texto"));
})
});
});
Para que funcione correctamente debe existir async a mas alto nivel del test unitario:
import App from "./App"
import { render, screen, expect } from "@testing-library/react";
describe("<Header />", () =>{
it("Should show title by Props", async () => {
await act(async() => {
render(<App/>);
screen.expect(await screen.findByText("Texto"));
})
});
});
Las variables de entorno que tenemos configuradas en nuestro proyecto (por ejemplo en nuestro archivo .env), deberán ser agregadas como Secrets en el repositorio de Github para que puedan ser utilizadas por las Github Action en el momento que se necesite. En éste caso, para poder ejecutar los test de manera correcta.
Configuración de Action Secrets en GitHub¶
En el apartado Seguridad
de las configuraciones del repositorio de GitHub, en la barra lateral izquierda, acceder a Actions
dentro de Secrets
.
El botón para agregar un nuevo secret para el repositorio nos permitirá setear la key y el valor de la variable de entorno que deseemos guardar. Se deberá crear una nueva secret por vada variable que se necesite o utilice en nuestro .env.
Una vez que hayamos agregado nuestras variables de entorno como secrets a nuestro repositorio, tendremos que agregarlas también en nuestro archivo de configuración de CI. Por lo general éste archivo (de extensión .yml) se encuentro dentro de una carpeta llamada .github.
Tener en cuenta que las key configuradas en CI deberán ser iguales a las que figuran en las secrets del repositorio.
env:
SKIP_TEST: 'false'
CI: true
REACT_APP_B2C_clientId: ${{ secrets.REACT_APP_B2C_clientId }}
REACT_APP_B2C_authorityDomain: ${{ secrets.REACT_APP_B2C_authorityDomain }}
REACT_APP_BASE_URL: ${{ secrets.REACT_APP_BASE_URL }}
Autor/a: Antony Fagundez
Contacto: afagundez@andreani.com