Saltar a contenido

Unit Testing Frontend

Pre-requisitos

  1. Leer las buenas prácticas para el desarrollo de Unit Test

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):

./__tests__/Header.test.tsx
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();
    });
});
Ejemplo de Snapshot generado
./__tests__/__snapshots__/Header.test.tsx.snap
// 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

./components/Header.jsx
import PropTypes from "prop-types"

export default function Header({ title }){
    return (
        <div>{title}</div>;
    )
};

Header.propTypes = {
    title: PropTypes.string.isRequired,
};

./__tests__/Header.test.jsx
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"));
        })
    });
});
## CICD

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.

repository-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.

repository-secrets

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