Proves
Tenir codi llest per a producció requereix fer proves. Com que no podem tenir milers de testers, sorgeix la necessitat d'automatitzar-les. Un procés que va de la mà de les pràctiques de desenvolupament àgil i el CD (Continuous delivery).
La piràmide de les proves
La piràmide de les proves és una metàfora visual que descriu les capes de les proves:
- Proves unitàries: validació dels blocs més atòmics del software, les funcions.
- Proves d'integració: validació que diferents peces del software funcionen juntes. Per exemple, comunicació amb la base de dades, la xarxa o el sistem d'arxius.
- Proves end to end (E2E): validació des del punt de vista de l'usuari, tractant el software com una caixa negra.
En el món real, aquesta categorització no és estricta, hi ha proves que poden estar al mig d'aquestes capes.
. | Unitàries | Integració | End-to-end |
---|---|---|---|
Objectiu principal | funcions individuals | integració de funcions | funcionalitat d'usuari |
Quantitat | Nombroses | Una mica freqüent | Escassa |
Velocitat | Molt alta | Mitjana | Lenta |
Freqüència d'execució | Alta, quan es desenvolupen una funció | Regular, quan es desenvolupa una funcionalitat | Quan s'acaba una funcionalitat |
Feedback | Entrada i sortida per a la funció | Comportament problemàtic | Funcionalitat incorrecta |
Cost | Baix: petita, fàcil d'actualitzar, executar i entendre | Moderat | Costós |
Coneixement de l'aplicació | Acoblat al codi | Codi, bases de dades, xarxa, arxius | Inconscient |
Beneficis | Feedback ràpid durant el desenvolupament, evitar regressions, documentació | Ús de llibreries de tercers, comprova efectes secundaris | Funcionament correcte de l'usuari |
Tècniques
Qualitat de les proves
Aquestes són algunes pistes per a aconseguir proves que siguin mantenibles:
- Les proves són tan importants com el codi.
- Prova només una funcionalitat per prova. Que sigui curta farà que sigui més clara.
- Escriure el nostre codi amb funcions petites ens ajudarà a fer proves més granulars.
- Estructura les proves amb "arrange, act, assert" o bé "given, when, then".
- La claredat és més important que no repetir-se.
Arrange, act i assert
L'estructura recomanada d'una prova és una seqüència de tres passos:
- Arrange: prepara les entrades i els objectius de la prova.
- Act: invoca el comportament, cridant una funció, interactuant amb un API, una pàgina web, etc.
- Assert: comprova que el resultat és l'esperat.
Test Doubles
Els test doubles són una tècnica que permet introduir una versió simplificada (o falsa) de les dades o funcions reals que permeten reduir la complexitat i facilitar les proves. Aquests objectes falsos poden classificar-se, de menys a més específic, en:
- Dummy: objectes que es passen però no s'utilitzen.
- Fake: implementacions que funcionen, però no serveixen per a producció.
- Stubs: proporcionen respostes enllaunades a certes preguntes, no responent a res que no hi hagi a la prova.
- Spies: són stubs que guarden informació de com van ser cridats.
- Mocks: estan programats amb expectatives, i per tant poden comprovar si la crida no s'espera, o falta alguna crida, llençant excepcions. Per tant, abans d'utilitzar-los cal indicar quines són les expectatives. Molts desenvolupadors utilitzen "mocks" per parlar de forma general dels dobles.
Millors pràctiques:
- Si es pot, evitar els dobles i treballar amb implementacions reals.
- De vegades no és possible, per diferents raons: volem provar certs escenaris (d'èxit o error) però no els podem generar, o bé és molt lent, o no volem treballar amb les dades reals. En general, escenaris on el codi té side effects.
- Llavors ens cal simular comportaments de certa dependència o servei extern.
- Entre els possibles dobles, es convenient seleccionar el menys específic (millor un dummy que un mock).
- Un cop seleccionat el doble, millor implementar-lo que utilitzar un framework.
Test-driven development
El TDD (desenvolupament guiat per proves) és una pràctica de desenvolupament que utilitza les proves unitàries per a escriure codi, i ho fa seguint el següent procediment:
- Afegir un nou test
- Executar tots els tests. La nova prova ha de fallar.
- Escriure el codi més senzill que permeti passar el test.
- Totes les proves han de funcionar novament.
- Fer refactoring quan calgui, utilitzant els tests per assegurar-se que la funcionalitat es preserva.
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
iuserEvent
, per a simular esdeveniments que permeten interactuar amb el DOM.act
iwaitFor
, 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:
- Les queries accessibles a tothom, amb
getByRole
,getByLabelText
,getByPlaceholderText
,getByText
,getByDisplayValue
. - Les queries semàntiques, amb
getByAltText
,getByTitle
. - Els testid, amb
getByTestId
. Implica l'ús de l'atributdata-testid
als elements. És el menys aconsellable, però evita l'ús d'altres atributs, com araid
oclass
, 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
ofalse
).checked
: per a la casella de selecció i els elements de ràdio (true
ofalse
).pressed
: per als elementsbutton
que indiquen l'estat de commutació (true
ofalse
).selected
: per a elements comoption
que indica l'estat de selecció (true
ofalse
).- `: 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.