Vitest o Jest: testare JavaScript nel 2026
Vitest o Jest a confronto: differenze reali, come si scrive un test in JavaScript, matcher, mocking, Testing Library con React e cosa vale la pena testare.
Decidi di iniziare a scrivere test sul tuo progetto JavaScript e la prima ora la passi su una schermata di configurazione invece che su un test. Jest o Vitest? Perché Jest si lamenta di import? In questo articolo trovi la differenza vera tra i due, come si scrive un test in pratica, perché il mocking fa perdere più tempo di tutto e cosa vale davvero la pena testare.
Sui tipi di test e sulla piramide non torno: sono già in cosa sono i test software, e per scrivere il test prima del codice c'è cos'è il TDD. Qui si sta sul pratico.
La differenza di fondo
Jest e Vitest fanno lo stesso mestiere — trovano i file di test, li eseguono e ti dicono cosa è fallito — ma Jest ha un proprio motore di trasformazione del codice, mentre Vitest riusa la configurazione di Vite che hai già nel progetto.
Tutto il resto discende da lì. L'API è quasi identica, i comandi si somigliano. Quello che cambia è chi trasforma il tuo codice moderno in qualcosa di eseguibile, e quanto quel pezzo ti dà fastidio.
Jest: lo standard storico
Jest è nato in Facebook ed è stato per anni il modo di testare JavaScript. Il suo vantaggio più grande non è tecnico: qualunque problema tu abbia, qualcuno lo ha già avuto e c'è una risposta scritta.
Il punto dolente è l'epoca in cui è nato. Jest è stato costruito in un mondo CommonJS, quello dei require(), e il passaggio ai moduli ES è arrivato dopo, con innesti successivi. Se il tuo progetto usa import o TypeScript, devi mettere in mezzo un trasformatore: Babel, ts-jest o swc.
E quel trasformatore è una seconda configurazione, indipendente da quella con cui costruisci il progetto: due posti dove dichiarare gli alias, due posti che possono andare fuori sincrono. Un import che funziona nell'app e fallisce nei test è l'esperienza classica. Non è insormontabile — milioni di progetti convivono con Jest benissimo — è solo lavoro che con Vitest non fai.
Vitest: la stessa API, la tua configurazione
Vitest riusa il vite.config.js del progetto: alias, plugin, trasformazioni di TypeScript e JSX sono già lì, e i test li ereditano. Se hai già Vite, la configurazione dei test è letteralmente questa:
// vite.config.js
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
test: {
environment: "jsdom",
globals: true,
setupFiles: "./src/test/setup.js",
},
});
Tre righe dentro un file che avevi già: questo è il vantaggio principale, e vale più della velocità.
L'API è compatibile con quella di Jest, e lo dico chiaramente perché è la domanda che si fanno tutti: describe, it, expect, i matcher, beforeEach, gli snapshot si scrivono uguali. La migrazione è quasi indolore: cambi il comando nel package.json, aggiungi gli import di vitest (o attivi globals: true) e sostituisci jest.fn() con vi.fn(). I file di test spesso non li tocchi.
L'altra differenza che si sente ogni giorno è il watch mode: salvi un file e Vitest rilancia solo i test che dipendono da quel file, in una frazione di secondo, perché non deve ricostruire niente.
Come si scrive un test
La struttura è la stessa con entrambi i runner. describe raggruppa, it (o test) descrive un comportamento, expect fa l'affermazione.
// src/lib/carrello.js
export function totale(articoli) {
return articoli.reduce((somma, a) => somma + a.prezzo * a.quantita, 0);
}
export function applicaSconto(importo, percentuale) {
if (percentuale < 0 || percentuale > 100) {
throw new Error("Percentuale non valida");
}
return Math.round(importo * (1 - percentuale / 100) * 100) / 100;
}
// src/lib/carrello.test.js
import { describe, it, expect } from "vitest";
import { totale, applicaSconto } from "./carrello";
describe("totale", () => {
it("somma prezzo per quantità", () => {
const articoli = [
{ prezzo: 10, quantita: 2 },
{ prezzo: 5.5, quantita: 1 },
];
expect(totale(articoli)).toBe(25.5);
});
it("restituisce 0 con il carrello vuoto", () => {
expect(totale([])).toBe(0);
});
});
describe("applicaSconto", () => {
it("applica la percentuale", () => {
expect(applicaSconto(100, 20)).toBe(80);
});
it("rifiuta le percentuali fuori range", () => {
expect(() => applicaSconto(100, 150)).toThrow("Percentuale non valida");
});
});
Nota il secondo e il quarto test: il carrello vuoto e l'input sbagliato. Il caso limite e il caso d'errore sono quasi sempre più utili del caso normale, che hai già provato a mano mentre scrivevi il codice.
I matcher che usi davvero
Sono molti, ne usi pochi:
| Matcher | Quando |
|---|---|
toBe(x) | Valori primitivi: numeri, stringhe, booleani |
toEqual(x) | Oggetti e array, confronto per contenuto |
toStrictEqual(x) | Come sopra, ma distingue anche undefined e classi |
toContain(x) | Un elemento dentro un array o una sottostringa |
toThrow(msg) | La funzione deve lanciare un errore |
toHaveBeenCalledWith(...) | Un mock è stato chiamato con certi argomenti |
rejects.toThrow() | Una promessa deve fallire |
L'errore più frequente è usare toBe su un oggetto: confronta l'identità, quindi due oggetti identici ma distinti falliscono. Per oggetti e array serve toEqual.
Il codice asincrono
Basta rendere il test async e usare await. Senza await, il test finisce prima dell'asserzione e passa comunque.
it("recupera l'utente", async () => {
const utente = await getUtente(1);
expect(utente.nome).toBe("Anna");
});
it("fallisce con un id inesistente", async () => {
await expect(getUtente(999)).rejects.toThrow("Non trovato");
});
Il mocking, e perché ti fa perdere più tempo di tutto il resto
Mockare significa sostituire una dipendenza vera con una finta: una API, l'orologio di sistema, il modulo che manda email.
import { describe, it, expect, vi, beforeEach } from "vitest";
import { inviaOrdine } from "./ordini";
import { postJson } from "./http";
vi.mock("./http");
beforeEach(() => {
vi.resetAllMocks();
});
it("invia l'ordine all'endpoint corretto", async () => {
postJson.mockResolvedValue({ id: 42 });
const risultato = await inviaOrdine({ articoli: [], totale: 0 });
expect(postJson).toHaveBeenCalledWith("/api/ordini", {
articoli: [],
totale: 0,
});
expect(risultato.id).toBe(42);
});
Funziona, ed è utile. Ma qui va fatto il punto onesto.
Più mocki, meno il test ti dice qualcosa di vero. Sopra stai verificando che la tua funzione chiami postJson con certi argomenti. Se domani il backend cambia il nome di quel campo, il test resta verde mentre in produzione gli ordini non partono: hai testato che il tuo codice parla con il tuo mock, e il mock è d'accordo per costruzione. È la trappola classica, 400 test verdi su un'applicazione rotta. Due regole:
- Mocka solo quello che sta oltre il confine del tuo sistema — rete, filesystem, orologio, servizi a pagamento. La logica interna testala vera.
- Se per testare una funzione devi mockare cinque cose, il problema è la funzione, che sa troppo: separa la logica pura dalle chiamate esterne. È uno dei modi in cui i test spingono verso il clean code.
Un'alternativa: invece di mockare il modulo HTTP, intercetta la rete con MSW (Mock Service Worker). Il tuo codice fa la fetch vera, con veri header e codici di stato, e a rispondere è un finto server.
Testare i componenti React
Con React lo strumento standard è Testing Library, e la sua filosofia va capita prima dell'API.
Interroga il DOM come farebbe un utente. Non cerchi lo stato interno del componente, non verifichi cosa contiene useState, non controlli i nomi delle classi CSS: cerchi il pulsante con scritto "Aggiungi al carrello", ci clicchi sopra e controlli che appaia "1 articolo".
Il motivo è pratico: un test scritto così sopravvive a un refactoring. Puoi riscrivere il componente da zero — se l'utente vede le stesse cose, il test resta verde. Un test che verifica gli interni si rompe a ogni modifica anche quando l'applicazione funziona, e i test che si rompono senza motivo vengono cancellati dopo due settimane.
import { describe, it, expect } from "vitest";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import Contatore from "./Contatore";
describe("Contatore", () => {
it("incrementa al clic", async () => {
const utente = userEvent.setup();
render(<Contatore iniziale={0} />);
expect(screen.getByText("Totale: 0")).toBeInTheDocument();
await utente.click(screen.getByRole("button", { name: /incrementa/i }));
expect(screen.getByText("Totale: 1")).toBeInTheDocument();
});
});
getByRole è la query da preferire: cerca l'elemento per il suo ruolo di accessibilità e per il testo che legge un utente. Se non trova niente, spesso non è un problema del test — è che quel pulsante non è accessibile nemmeno a un lettore di schermo. Il test ti sta dicendo qualcosa di utile sul DOM che hai prodotto.
Cosa vale la pena testare (e cosa no)
La domanda che conta più della scelta del runner.
Merita un test:
- La logica di dominio: prezzi, sconti, IVA, date, permessi, validazioni. Regole che si possono sbagliare in silenzio.
- Le funzioni con molti rami e casi limite. Se c'è un
ifc'è un test. - Ogni bug risolto. Prima il test che lo riproduce, poi la correzione: è l'unico modo per essere certo che non torni.
- I componenti con comportamento vero: form con validazione, liste filtrabili, stati di caricamento ed errore.
Non lo merita:
- I getter banali e i componenti che rendono solo delle props in un
div: il test ripete il codice e si rompe insieme a lui. - Le librerie che usi. React funziona, il tuo router funziona: non li stai testando tu.
- Il 100% di copertura. Oltre una certa soglia scrivi test per far salire un numero: la copertura serve a scoprire cosa hai dimenticato, non come obiettivo.
- Gli snapshot a raffica. Uno snapshot che nessuno legge è un rituale: fallisce, premi il tasto per aggiornarlo, fine.
Il confronto
| Jest | Vitest | |
|---|---|---|
| Configurazione | File a parte, più trasformatore | Riusa vite.config.js |
| Moduli ES e TypeScript | Serve Babel, ts-jest o swc | Nativi |
| Watch mode | Rilancia, può essere lento | Quasi istantaneo |
| API | Lo standard di fatto | Compatibile con quella di Jest |
| Ecosistema e guide | Enorme, ogni caso è documentato | Ampio e in crescita |
| Fuori da Vite | Funziona ovunque | Funziona, ma perde il vantaggio principale |
| Testing Library | Supportata | Supportata |
Errori comuni che fanno perdere ore
- Non pulire i mock tra un test e l'altro. Un test passa da solo e fallisce nella suite: quasi sempre è uno stato che sopravvive.
vi.resetAllMocks()inbeforeEach(oclearMocks: true) risolve la maggior parte dei casi. - Dimenticare
awaitsu un'asserzione asincrona. Il test passa sempre, anche col codice rotto. Se un test non fallisce mai, sospetta. - Alias dei percorsi che funzionano nell'app e non nei test. Con Jest è il classico:
@/componentsva dichiarato anche inmoduleNameMapper. Con Vitest non succede, perché la configurazione è una sola. - Ambiente sbagliato. Testi un componente e
documentèundefined: mancaenvironment: "jsdom". Per il codice Node.js usanode. - Test dipendenti dall'ordine. Se il terzo
itha bisogno del secondo, la suite salta appena qualcuno lancia i test in parallelo. - Date e orologio veri. Un test con
new Date()fallirà a Capodanno o alle 23:59. Congela il tempo convi.useFakeTimers().
Il verdetto
Progetto nuovo che usa Vite: Vitest, senza pensarci. La configurazione è già fatta e l'API è quella che conosci.
Progetto esistente con Jest che funziona: non migrare per moda. Una migrazione costa giorni e non aggiunge una sola funzionalità al prodotto. Ha senso se stai già soffrendo — configurazione dei moduli ES fragile, suite così lenta da non lanciarla più.
Progetto Node puro senza Vite: vanno bene entrambi, e per una libreria piccola basta il runner integrato in Node (node --test). Con Next.js ci sono guide ufficiali per tutti e due.
In sintesi
Jest e Vitest fanno la stessa cosa e si scrivono quasi uguale. La differenza vera è che Vitest riusa la configurazione di Vite, risparmiandoti il problema dei moduli ES e di TypeScript.
La migrazione è quasi indolore, ma "quasi indolore" non vuol dire "gratis": se Jest funziona, lascialo stare.
Il mocking fa perdere più tempo e più valore di ogni altra cosa. Mocka solo ciò che sta oltre il confine del tuo sistema: test verdi su un'app rotta è uno scenario reale.
Con Testing Library interroghi il DOM come un utente, non gli interni del componente. È quello che rende i test resistenti al refactoring.
Non testare tutto. Testa la logica che può sbagliarsi in silenzio, i casi limite e ogni bug già capitato. La copertura è un indizio, non un obiettivo.
Il passo successivo è farli girare a ogni push: cos'è la CI/CD e cosa sono le GitHub Actions. Per i test che aprono un browser vero, cos'è Playwright.
Contenuto redatto con l'assistenza di strumenti di intelligenza artificiale e rivisto dalla redazione di Codegrind, che ne è responsabile. Hai trovato un errore? Scrivici.