Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Proves amb JavaScript

Aquesta pàgina aplica a JavaScript els conceptes generals de Proves: la piràmide de proves, l’estructura arrange, act i assert, els test doubles i el TDD. Si no els has llegit, comença per allà.

Eines

Jest i Vitest

El framework de proves JS més conegut és Jest. L’alternativa més interessant és Vitest, completament compatible amb Jest, més ràpid i que funciona amb mòduls.

Les proves JS s’emparellen amb el codi que es vol testar. Per exemple, feature.js tindria les proves a feature.test.js. Dins d’aquest arxiu s’han de poder trobar crides del tipus:

test('descripció de la prova', () => {
  // codi de la prova (arrange, act, assert)
});

Alternativament, també s’utilitza la funció it (sinònim) en lloc de test. Podem utilitzar les següents funcions per a actuar abans i després de les proves:

beforeAll(() => {
  // s'executa abans de totes les proves
});
beforeEach(() => {
  // s'executa abans de cada prova
});
afterEach(() => {
  // s'executa després de cada prova
});
afterAll(() => {
  // s'executa després de totes les proves
});

Arrange

L’arrange ha de preparar les entrades i els objectius de la prova. Això pot incloure la selecció dels nodes del DOM, la creació d’objectes, la configuració de l’estat inicial, la creació de mocks, etc.

Per a fer la selecció dels nodes, podem utilitzar document.querySelector o document.querySelectorAll. Però aquesta no és la millor forma, ja que no és el que faria un usuari (veure la Testing Library).

Act

Per al pas act, podríem utilitzar JavaScript directament, però és més recomanable utilitzar una llibreria que simuli l’usuari (veure la Testing Library).

Assert

Per al pas assert, tenim l’estructura expect(receivedValue).matcher(expectedValue). Els matchers més habituals són els següents:

  • toBe: per a comprovar valors primitius.
  • toEqual: per a comprovar objectes o arrays.
  • not.matcher(expectedValue): per a negar qualsevol matcher.
  • toBeNull, toBeUndefined, toBeDefined, toBeTruthy, toBeFalsy: per a comprovar truthiness.
  • toBeGreaterThan, toBeGreaterThanOrEqual, toBeLessThan, toBeLessThanOrEqual: per a números.
  • toMatch(/.../): per a expressions regulars.
  • toContain: per a comprovar si un array o iterable conté un ítem.
  • toThrow: per a comprovar si es llença una excepció. Permet comprovar el text també.

Per exemple, expect(2 + 2).toBe(4) comprova que 2+2 són 4.

La Testing Library afegeix nous matchers a jest.

Testing Library

La Testing Library és un complement a jest o vitest que afegeix noves funcionalitats per a fer proves.

Aquesta llibreria conté:

  • Queries, uns mètodes per trobar elements a una pàgina. getBy, queryBy i findBy.
  • fireEvent i userEvent, per a simular esdeveniments que permeten interactuar amb el DOM.
  • act i waitFor, per a esperar rerenders o actualitzacions d’estat asíncrones.
  • Matchers addicionals per a jest/vitest

A més, és aplicable tan a aplicacions vanilla JS com a React o d’altres frameworks.

Queries

Les queries poden ser de tres tipus, segons els seu comportament:

  • getBy: retorna un element o llença una excepció si no el troba.
  • queryBy: retorna un element o null si no el troba.
  • findBy: retorna una promesa que es resoldrà quan trobi l’element.

També hi ha les queries per a múltiples elements:

  • getAllBy: retorna un array d’elements o llença una excepció si no en troba cap.
  • queryAllBy: retorna un array d’elements o un array buit si no en troba cap.
  • findAllBy: retorna una promesa que es resoldrà quan trobi els elements.

El format get és el més recomanat, ja que si no troba l’element, llença una excepció i atura la prova. Es recomana utilitzar les queries en aquest ordre:

  1. Les queries accessibles a tothom, amb getByRole, getByLabelText, getByPlaceholderText, getByText, getByDisplayValue.
  2. Les queries semàntiques, amb getByAltText, getByTitle.
  3. Els testid, amb getByTestId. Implica l’ús de l’atribut data-testid als elements. És el menys aconsellable, però evita l’ús d’altres atributs, com ara id o class, associats normalment a l’estil.

Exemples de queries:

const button = getByRole('button', { name: /submit/i });
const input = getByLabelText('Username');
const input = getByPlaceholderText('Username');
const text = getByText('Hello, world!');
const input = getByDisplayValue('Hello, world!');
const img = getByAltText('A close-up of a cat');
const element = getByTitle('A description of the element');

Les queries del getByRole() permeten buscar dins d’un rol. Hi ha un nombre de rols establers per l’estàndard WAI-Aria. Alguns elements tenen un valor implícit, i també es poden definir amb l’atribut role d’un element HTML.

Les opcions més comunes d’aquesta funció són:

  • name: relaciona els elements pel seu nom accessible.
  • level: s’utilitza per a les funcions d’encapçalament per especificar nivells (h1, h2, etc.).
  • expanded: per a elements ampliables (true o false).
  • checked: per a la casella de selecció i els elements de ràdio (true o false).
  • pressed: per als elements button que indiquen l’estat de commutació (true o false).
  • selected: per a elements com option que indica l’estat de selecció (true o false).
  • hidden: inclou elements que estan visualment ocults però encara accessibles.

Alguns examples de queries:

screen.getByRole("button", { name: "Click Me" });
screen.getByRole("heading", { level: 2 });
screen.getByRole("combobox", { expanded: true });
screen.getByRole("button", { name: "Submit" });
screen.getByRole("option", { selected: true });

El name de les opcions es pot derivar de (en ordre de preferència):

  • aria-label
  • aria-labelledby
  • Text intern (per botons, capçaleres, etc.)
  • Atribut alt (imatges)
  • Elements label associats (per a form inputs, etc.)
  • Atribut title (si no es troba cap altre)

fireEvent i userEvent

fireEvent és una funció que permet simular esdeveniments. userEvent és una llibreria que permet simular esdeveniments com ho faria un usuari.

Per exemple, fireEvent.click(button) simula un clic sobre un botó. userEvent.click(button) també simula un clic sobre un botó, però ho fa com ho faria un usuari.

Es recomana utilitzar userEvent sempre que sigui possible, ja que simula millor el comportament d’un usuari.

act i waitFor

act permet esperar fins que s’han produït tots els rerenders associats a les accions sobre el DOM. Moltes funcions de la testing library ja l’utilitzen, com per exemple fireEvent. Però en altres casos, com per exemple quan s’interactua amb un custom hook, cal utilitzar-lo perquè cal que l’estat s’actualitzi abans de tornar a generar un esdeveniment.

waitFor és una funció que permet esperar fins que una condició asíncrona es compleixi. Per exemple, waitFor(() => getByText('text')). Aquesta funció és útil quan es vol esperar fins que un element aparegui a la pàgina, o fins que desaparegui. També és útil per a esperar fins que un element canvii.

La sintaxi és:

await waitFor(() => {
  // codi que comprova la condició
});

Per canviar el temps esperat, es pot passar un objecte amb la propietat timeout:

await waitFor(() => {
  // codi que comprova la condició
}, { timeout: 1000 });

Matchers addicionals

Aquesta llibreria també afegeix matchers addicionals a jest, com ara toBeVisible, toBeDisabled, toBeEmpty, toBeEnabled, toBeInvalid, toBeRequired, toBeValid, toBeVisible, toHaveTextContent, toHaveValue, toHaveAttribute, toHaveClass, toHaveStyle, toHaveFormValues, toBeChecked, toBePartiallyChecked, toHaveFocus, toHaveDescription, toHaveDisplayValue, toHaveDisplayValue, toHaveErrorMessage, toHaveLabel, toHaveLabelText, toHaveRole, toHaveTitle, toHaveAltText, toHaveAriaLabel, toHaveAriaRole, toHaveAriaSelected.

JSDOM

Es tracta de la implementació JavaScript d’un navegador sense interfície i molt més ràpid que els convencionals. Permet implementar proves automatitzades amb poca infraestructura i senzillesa.

La seva configuració es mostra en l’apartat corresponent d’aquest document. Implementa l’objecte window.document i les seves API.

Renderització

Components

Per provar components ho podem fer amb render. Cal cridar-lo abans de fer qualsevol comprovació de l’estat. Per exemple:

render(<Clickdown initialValue={3} />);
const titleEl = screen.getByText(/value is 3/i);
expect(titleEl).toBeInTheDocument();

També podríem tornar a renderitzar el component simulant que ha canviat una prop. O bé desmuntar un component per a comprovar l’efecte:

const { rerender, unmount } = render(<Clickdown initialValue={3} />);
// ...
rerender(<Clickdown initialValue={4} />); // then, check the new value...
unmount(); // then check the effect of unmounting

Hooks

Per provar els custom hooks podem utilitzar renderHook. Per exemple, si tenim un hook del comptador:

const useCounter = (initialState: number) => {
  const [counter, setCounter] = useState<number>(initialState);
  return {
    value: counter,
    increment() {
      setCounter(prev => prev + 1);
    }
  };
};

El podríem provar així:

const { result } = renderHook(() => useCounter(0));
act(() => result.current.increment());
expect(result.current.value).toBe(1);

Com es pot veure, s’utilitza act per assegurar-se que el canvi d’estat associat a increment s’ha consolidat. També podem utilitzar waitFor per a esperar un temps un esdeveniment asíncron.

L’objecte retornat per renderHook també conté rerender i unmount, tal com passa amb el render dels components.

Configuració

Vanilla JS

La llibreria més fàcil d’utilitzar és Vitest, compatible amb la sintaxi de Jest i amb ES modules. Necessitarem npm per a fer les proves, i això requereix crear un package.json:

$ npm init -y
$ npm install --save-dev vitest jsdom @testing-library/dom @testing-library/jest-dom

Caldria editar l’entrada script del nostre package.json:

"test": "vitest --run --reporter verbose"

Si necessitem provar el DOM, caldria afegir l’environment jsdom a la capçalera de qualsevol arxiu de proves. La sintaxi de Vitest és la de Jest:

// @jest-environment jsdom

Podem fer npm test per a executar els tests.

Per a fer proves DOM amb un script utilitzant Vitest, l’arrange podria ser:

const initialHtml = fs.readFileSync("./index.html", "utf-8");
document.body.innerHTML = initialHtml;
vi.resetModules(); // recarrega mòduls
await import('./script.js'); // aplica un script que estaria a l'HTML

També és interessant importar això si volem afegir els custom matchers de jest a les nostres proves:

import '@testing-library/jest-dom/vitest';

React

Utilitzarem vitest per a les proves React.

Caldria instal.lar vitest, @testing-library/react i @testing-library/jest-dom. A més, si volem afegir matchers addicionals, podem fer-ho important-los des de vite.config.js:

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  test: {
    environment: 'jsdom',
    setupFiles: './src/setup_tests.js',
  },  
});

Llavors, setup_tests.js podria ser:

import { afterEach } from 'vitest';
import { cleanup } from '@testing-library/react';
import '@testing-library/jest-dom/vitest';

afterEach(() => {
  cleanup(); // clears the jsdom after each test
});

Podem canviar l’entrada test del package.json perquè mostri els detalls dels tests:

"test": "vitest --run --reporter verbose",

Podem fer npm test per a executar els tests.

Referències

Last change: , commit: 7ca2d12