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, cerchi da dove cominciare, 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, come funziona il mocking e perché è la parte che fa perdere più tempo, e — soprattutto — cosa vale davvero la pena testare.
Sui tipi di test e sulla piramide non torno: sono già in cosa sono i test software, e se ti interessa 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, i report a schermo pure. Quello che cambia è chi si occupa di trasformare 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 da qualche parte.
Il punto dolente è l'epoca in cui è nato. Jest è stato costruito in un mondo CommonJS, quello dei require(), e il passaggio ai moduli ES — gli import — è arrivato dopo, con innesti successivi. Se il tuo progetto usa import, o TypeScript, o entrambi, devi mettere in mezzo un trasformatore: Babel, ts-jest o swc.
E quel trasformatore è una seconda configurazione, parallela e indipendente da quella con cui costruisci il progetto. Due posti dove dichiarare gli alias dei percorsi, due posti dove dire come gestire il CSS importato, due posti che possono andare fuori sincrono. Un import che funziona quando lanci l'app e fallisce quando lanci i 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. Gli alias, i plugin, le 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: nella maggior parte dei casi cambi il comando nel package.json, aggiungi gli import di vitest (o attivi globals: true per non doverlo fare) 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. Vitest riusa il server di sviluppo di Vite: salvi un file e rilancia solo i test che dipendono da quel file, in una frazione di secondo. Non è marketing, è che non deve ricostruire niente.
Come si scrive un test
La struttura è sempre la stessa, indipendentemente dal 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, perché il caso normale lo 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. Nell'esempio sopra stai verificando che la tua funzione chiami postJson con certi argomenti. Se domani il backend cambia il nome di quel campo, o l'endpoint risponde 400, 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, una suite di 400 test verdi su un'applicazione rotta. Due regole che aiutano:
- Mocka solo quello che sta oltre il confine del tuo sistema — rete, filesystem, orologio, servizi a pagamento. La tua logica interna testala vera.
- Se per testare una funzione devi mockare cinque cose, il problema non è il test. È 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 veri codici di stato, e a rispondere è un finto server. Resta veloce ma molto più realistico.
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 che useState contenga un certo valore, 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, cambiare gli hook, rinominare tutto — 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 che hai 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, non per trovare bug: la copertura serve a scoprire cosa hai dimenticato, non come obiettivo.
- Gli snapshot a raffica. Uno snapshot che nessuno legge diventa 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, o viceversa: quasi sempre è uno stato che sopravvive.
vi.resetAllMocks()inbeforeEach(oclearMocks: truenella configurazione) risolve la maggior parte dei casi. - Dimenticare
awaitsu un'asserzione asincrona. Il test passa sempre, anche quando il 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 usa invecenode. - Test dipendenti dall'ordine. Se il terzo
itha bisogno che il secondo sia passato prima, la suite salta appena qualcuno lancia i test in parallelo o filtrati. - Date e orologio veri. Un test che usa
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, il watch mode è un altro sport e l'API è quella che conosci già.
Progetto esistente con Jest che funziona: non migrare per moda. Una migrazione costa giorni, introduce rischio 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ù — non perché Vitest è lo strumento del momento.
Progetto Node puro senza Vite: vanno bene entrambi, e per una libreria piccola basta il test runner integrato in Node (node --test). Con Next.js trovi 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.