Guida al testing con Vitest: da zero alla fiducia in produzione
Unit test, integration test, component test con Testing Library, strategie di mocking, configurazione della coverage e la filosofia di testing che produce davvero software migliore.
La maggior parte dei team scrive test come la maggior parte delle persone fa esercizio: sanno che dovrebbero, si sentono in colpa quando non lo fanno, e quando finalmente lo fanno, esagerano il primo giorno e mollano entro venerdì. Il codebase finisce con una manciata di snapshot test superficiali che si rompono ogni volta che qualcuno cambia una classe CSS, qualche integration test di cui nessuno si fida, e un badge di coverage nel README che mente.
Sono stato su entrambi i fronti. Ho rilasciato progetti con zero test e ho sudato freddo a ogni deploy. Sono stato anche in team che inseguivano il 100% di coverage e passavano più tempo a mantenere i test che a scrivere feature. Nessuna delle due funziona. Quello che funziona è scrivere i test giusti, nei posti giusti, con gli strumenti giusti.
Vitest ha cambiato il mio modo di pensare al testing in JavaScript. Non perché ha inventato concetti nuovi — i fondamentali non sono cambiati da quando Kent Beck ne scrisse decenni fa. Ma perché ha rimosso abbastanza attrito da far sì che scrivere test non sembrasse più un compito noioso e diventasse parte del ciclo di sviluppo. Quando il tuo test runner è veloce quanto il tuo dev server e usa la stessa configurazione, le scuse evaporano.
Questo post è tutto ciò che so sul testing con Vitest, dal setup iniziale alla filosofia che rende tutto utile.
Perché Vitest invece di Jest#
Se hai usato Jest, conosci già la maggior parte dell'API di Vitest. È per design — Vitest è compatibile con Jest a livello di API. describe, it, expect, beforeEach, vi.fn() — funziona tutto. Allora perché cambiare?
Supporto ESM nativo#
Jest è stato costruito per CommonJS. Può gestire ESM, ma richiede configurazione, flag sperimentali e qualche preghiera. Se usi la sintassi import/export (che è tutto ciò che è moderno), probabilmente hai combattuto con la pipeline di trasformazione di Jest.
Vitest gira su Vite. Vite capisce ESM nativamente. Non c'è nessun passaggio di trasformazione per il tuo codice sorgente — funziona e basta. Questo conta più di quanto sembri. Metà dei problemi di Jest che ho debuggato nel corso degli anni risalgono alla risoluzione dei moduli: SyntaxError: Cannot use import statement outside a module, o mock che non funzionano perché il modulo era già cachato in un formato diverso.
Stessa configurazione del dev server#
Se il tuo progetto usa Vite (e se stai costruendo un'app React, Vue o Svelte nel 2026, probabilmente lo usa), Vitest legge il tuo vite.config.ts automaticamente. I tuoi alias dei percorsi, plugin e variabili d'ambiente funzionano nei test senza configurazione aggiuntiva. Con Jest, mantieni una configurazione parallela che deve restare sincronizzata con il setup del tuo bundler.
Velocità#
Vitest è veloce. Significativamente veloce. Non "ti fa risparmiare due secondi" veloce — "cambia il tuo modo di lavorare" veloce. Usa il module graph di Vite per capire quali test sono influenzati da una modifica a un file e esegue solo quelli. La sua watch mode usa la stessa infrastruttura HMR che rende istantaneo il dev server di Vite.
Su un progetto con oltre 400 test, passare da Jest a Vitest ha tagliato il nostro ciclo di feedback in watch mode da circa 4 secondi a sotto i 500ms.
Benchmarking integrato#
Vitest include bench() out of the box per i test di performance:
import { bench, describe } from "vitest";
describe("string concatenation", () => {
bench("template literals", () => {
const name = "world";
const _result = `hello ${name}`;
});
bench("string concat", () => {
const name = "world";
const _result = "hello " + name;
});
});Esegui con vitest bench.
Setup#
Installazione#
npm install -D vitest @testing-library/react @testing-library/jest-dom @testing-library/user-event jsdomConfigurazione#
Crea vitest.config.ts nella root del progetto:
import { defineConfig } from "vitest/config";
import react from "@vitejs/plugin-react";
import path from "path";
export default defineConfig({
plugins: [react()],
test: {
globals: true,
environment: "jsdom",
setupFiles: ["./src/test/setup.ts"],
include: ["src/**/*.{test,spec}.{ts,tsx}"],
alias: {
"@": path.resolve(__dirname, "./src"),
},
css: false,
},
});Oppure, se hai già un vite.config.ts, puoi estenderlo:
/// <reference types="vitest/config" />
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
"@": "/src",
},
},
test: {
globals: true,
environment: "jsdom",
setupFiles: ["./src/test/setup.ts"],
},
});La decisione globals: true#
Quando globals è true, non hai bisogno di importare describe, it, expect, beforeEach, ecc. — sono disponibili ovunque, proprio come in Jest. Quando è false, li importi esplicitamente:
// globals: false
import { describe, it, expect } from "vitest";
describe("math", () => {
it("adds numbers", () => {
expect(1 + 1).toBe(2);
});
});Se usi globals: true, aggiungi i tipi di Vitest al tuo tsconfig.json:
{
"compilerOptions": {
"types": ["vitest/globals"]
}
}Ambiente: jsdom vs happy-dom vs node#
Vitest ti permette di scegliere l'implementazione del DOM per test o globalmente:
node— Nessun DOM. Per logica pura, utility, route API e tutto ciò che non tocca il browser.jsdom— Lo standard. Implementazione DOM completa. Più pesante ma più completa.happy-dom— Più leggero e veloce di jsdom ma meno completo.
Io uso jsdom come default globale e sovrascrivo per file quando serve node:
// src/lib/utils.test.ts
// @vitest-environment node
import { formatDate, slugify } from "./utils";
describe("slugify", () => {
it("converts spaces to hyphens", () => {
expect(slugify("hello world")).toBe("hello-world");
});
});File di setup#
Il file di setup viene eseguito prima di ogni file di test. Qui configuri i matcher di Testing Library e i mock globali:
// src/test/setup.ts
import "@testing-library/jest-dom/vitest";
// Mock IntersectionObserver — jsdom non lo implementa
class MockIntersectionObserver {
observe = vi.fn();
unobserve = vi.fn();
disconnect = vi.fn();
}
Object.defineProperty(window, "IntersectionObserver", {
writable: true,
value: MockIntersectionObserver,
});
// Mock window.matchMedia — necessario per componenti responsive
Object.defineProperty(window, "matchMedia", {
writable: true,
value: vi.fn().mockImplementation((query: string) => ({
matches: false,
media: query,
onchange: null,
addListener: vi.fn(),
removeListener: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
})),
});Scrivere buoni test#
Il pattern AAA#
Ogni test segue la stessa struttura: Arrange, Act, Assert. Anche quando non scrivi commenti espliciti, la struttura dovrebbe essere visibile:
it("calculates total with tax", () => {
// Arrange
const items = [
{ name: "Widget", price: 10 },
{ name: "Gadget", price: 20 },
];
const taxRate = 0.08;
// Act
const total = calculateTotal(items, taxRate);
// Assert
expect(total).toBe(32.4);
});Naming dei test#
Ci sono due scuole: it('should calculate total with tax') e it('calculates total with tax'). Il prefisso "should" è verboso senza aggiungere informazioni. Preferisco la forma diretta: it('renders loading state'), it('rejects invalid email'), it('returns empty array when no matches found').
Per i blocchi describe, usa il nome dell'unità sotto test:
describe("calculateTotal", () => {
it("sums item prices", () => { /* ... */ });
it("applies tax rate", () => { /* ... */ });
it("returns 0 for empty array", () => { /* ... */ });
it("handles negative prices", () => { /* ... */ });
});Leggilo ad alta voce: "calculateTotal sums item prices." Se la frase funziona, il naming funziona.
Un'asserzione per test vs raggruppamento pratico#
La regola purista dice un'asserzione per test. La regola pratica dice: un concetto per test. Sono cose diverse.
// Questo va bene — un concetto, multiple asserzioni
it("formats user display name", () => {
const user = { firstName: "John", lastName: "Doe", title: "Dr." };
const result = formatDisplayName(user);
expect(result).toContain("John");
expect(result).toContain("Doe");
expect(result).toStartWith("Dr.");
});
// Questo non va bene — concetti multipli in un test
it("handles user operations", () => {
const user = createUser("John");
expect(user.id).toBeDefined();
const updated = updateUser(user.id, { name: "Jane" });
expect(updated.name).toBe("Jane");
deleteUser(user.id);
expect(getUser(user.id)).toBeNull();
});Mocking#
Il mocking è lo strumento più potente e più pericoloso nel tuo toolkit di testing. Usato bene, isola l'unità sotto test e rende i test veloci e deterministici. Usato male, crea test che passano qualunque cosa faccia il codice.
vi.fn() — Creare funzioni mock#
Il mock più semplice è una funzione che registra le sue chiamate:
const mockCallback = vi.fn();
// Chiamala
mockCallback("hello", 42);
mockCallback("world");
// Asserisci sulle chiamate
expect(mockCallback).toHaveBeenCalledTimes(2);
expect(mockCallback).toHaveBeenCalledWith("hello", 42);
expect(mockCallback).toHaveBeenLastCalledWith("world");vi.spyOn() — Osservare metodi reali#
Quando vuoi osservare un metodo senza sostituire il suo comportamento:
const consoleSpy = vi.spyOn(console, "warn");
validateInput("");
expect(consoleSpy).toHaveBeenCalledWith("Input cannot be empty");
consoleSpy.mockRestore();vi.mock() — Mocking a livello di modulo#
Questo è il pezzo forte. vi.mock() sostituisce un intero modulo:
// Mock dell'intero modulo
vi.mock("@/lib/api", () => ({
fetchUsers: vi.fn().mockResolvedValue([
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" },
]),
fetchUser: vi.fn().mockResolvedValue({ id: 1, name: "Alice" }),
}));
// L'import ora usa la versione mockata
import { fetchUsers } from "@/lib/api";
describe("UserList", () => {
it("displays users from API", async () => {
const users = await fetchUsers();
expect(users).toHaveLength(2);
});
});Clear vs Reset vs Restore#
Questo confonde tutti prima o poi:
const mockFn = vi.fn().mockReturnValue(42);
mockFn();
// mockClear — resetta la cronologia delle chiamate, mantiene l'implementazione
mockFn.mockClear();
expect(mockFn).not.toHaveBeenCalled(); // true
expect(mockFn()).toBe(42); // restituisce ancora 42
// mockReset — resetta cronologia E implementazione
mockFn.mockReset();
expect(mockFn()).toBeUndefined(); // non restituisce più 42
// mockRestore — per gli spy, ripristina l'implementazione originale
const spy = vi.spyOn(Math, "random").mockReturnValue(0.5);
spy.mockRestore();
// Math.random() ora funziona normalmenteIl pericolo dell'over-mocking#
Questo è il consiglio più importante sul mocking che posso dare: ogni mock è una bugia che racconti al tuo test. Quando mocki una dipendenza, stai dicendo "mi fido che questa cosa funzioni correttamente, quindi la sostituisco con una versione semplificata." Se la tua assunzione è sbagliata, il test passa ma la feature è rotta.
La regola: mocka ai confini, non nel mezzo. Mocka le richieste di rete, l'accesso al file system e i servizi di terze parti. Non mockare le tue funzioni utility a meno che non ci sia un motivo valido.
Testare componenti React#
Le basi con Testing Library#
Testing Library impone una filosofia: testa i componenti nel modo in cui gli utenti interagiscono con essi. Nessun controllo dello stato interno, nessuna ispezione delle istanze dei componenti, nessun shallow rendering. Renderizzi un componente e interagisci con esso attraverso il DOM, come farebbe un utente.
import { render, screen } from "@testing-library/react";
import { Button } from "@/components/ui/Button";
describe("Button", () => {
it("renders with label text", () => {
render(<Button>Click me</Button>);
expect(screen.getByRole("button", { name: "Click me" })).toBeInTheDocument();
});
it("applies variant classes", () => {
render(<Button variant="primary">Submit</Button>);
const button = screen.getByRole("button");
expect(button).toHaveClass("bg-primary");
});
it("is disabled when disabled prop is true", () => {
render(<Button disabled>Submit</Button>);
expect(screen.getByRole("button")).toBeDisabled();
});
});Query: getBy vs queryBy vs findBy#
Qui è dove i principianti si confondono. Ci sono tre varianti di query e ognuna ha un caso d'uso specifico:
getBy* — Restituisce l'elemento o lancia un errore se non trovato. Usalo quando ti aspetti che l'elemento esista.
queryBy* — Restituisce l'elemento o null se non trovato. Usalo quando asserisci che qualcosa NON è presente:
expect(screen.queryByText("Error message")).not.toBeInTheDocument();findBy* — Restituisce una Promise. Usalo per elementi che appaiono in modo asincrono:
const successMessage = await screen.findByText("Saved successfully");
expect(successMessage).toBeInTheDocument();Query accessibility-first#
Testing Library fornisce queste query in un ordine di priorità deliberato:
getByRole— La migliore query. Usa i ruoli ARIA.getByLabelText— Per gli elementi dei form.getByPlaceholderText— Accettabile ma più debole.getByText— Per elementi non interattivi.getByTestId— Ultima risorsa.
// Preferisci questo
screen.getByRole("textbox", { name: "Email address" });
// Rispetto a questo
screen.getByPlaceholderText("Enter your email");
// E sicuramente rispetto a questo
screen.getByTestId("email-input");User event#
Non usare fireEvent. Usa @testing-library/user-event. La differenza conta:
import userEvent from "@testing-library/user-event";
describe("SearchInput", () => {
it("filters results as user types", async () => {
const user = userEvent.setup();
const onSearch = vi.fn();
render(<SearchInput onSearch={onSearch} />);
const input = screen.getByRole("searchbox");
await user.type(input, "vitest");
// user.type lancia keydown, keypress, input, keyup per OGNI carattere
// fireEvent.change imposta solo il valore — saltando il flusso realistico degli eventi
expect(onSearch).toHaveBeenLastCalledWith("vitest");
});
});Testare codice asincrono#
waitFor#
Quando un componente si aggiorna in modo asincrono, waitFor interroga ripetutamente finché l'asserzione non passa:
import { render, screen, waitFor } from "@testing-library/react";
it("loads and displays user profile", async () => {
render(<UserProfile userId="123" />);
// Inizialmente mostra il caricamento
expect(screen.getByText("Loading...")).toBeInTheDocument();
// Aspetta che appaia il contenuto
await waitFor(() => {
expect(screen.getByText("John Doe")).toBeInTheDocument();
});
// L'indicatore di caricamento dovrebbe essere sparito
expect(screen.queryByText("Loading...")).not.toBeInTheDocument();
});Fake timer#
Quando testi codice che usa setTimeout, setInterval o Date, i fake timer ti permettono di controllare il tempo:
describe("Debounce", () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it("delays execution by the specified time", () => {
const callback = vi.fn();
const debounced = debounce(callback, 300);
debounced();
expect(callback).not.toHaveBeenCalled();
vi.advanceTimersByTime(200);
expect(callback).not.toHaveBeenCalled();
vi.advanceTimersByTime(100);
expect(callback).toHaveBeenCalledOnce();
});
});Importante: chiama sempre vi.useRealTimers() in afterEach. I fake timer che leakano tra i test causano i fallimenti più confusi che tu possa mai debuggare.
Mocking delle API con MSW#
Per testare il data fetching, Mock Service Worker (MSW) intercetta le richieste di rete a livello di rete. Questo significa che il codice fetch/axios del tuo componente viene eseguito esattamente come in produzione — MSW sostituisce solo la risposta di rete:
import { http, HttpResponse } from "msw";
import { setupServer } from "msw/node";
import { render, screen } from "@testing-library/react";
const server = setupServer(
http.get("/api/users", () => {
return HttpResponse.json([
{ id: 1, name: "Alice", email: "alice@example.com" },
{ id: 2, name: "Bob", email: "bob@example.com" },
]);
})
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
describe("UserList", () => {
it("displays users from API", async () => {
render(<UserList />);
expect(await screen.findByText("Alice")).toBeInTheDocument();
expect(await screen.findByText("Bob")).toBeInTheDocument();
});
it("shows error state when API fails", async () => {
server.use(
http.get("/api/users", () => {
return new HttpResponse(null, { status: 500 });
})
);
render(<UserList />);
expect(await screen.findByText(/failed to load/i)).toBeInTheDocument();
});
});MSW è meglio che mockare fetch o axios direttamente perché:
- Il codice reale di data fetching del tuo componente viene eseguito — testi l'integrazione reale.
- Puoi testare la gestione degli errori sovrascrivendo gli handler per singolo test.
- Gli stessi handler funzionano sia nei test che nel browser dev mode.
Integration test#
Unit test vs integration test#
Uno unit test isola una singola funzione o componente e mocka tutto il resto. Un integration test lascia che più unità lavorino insieme e mocka solo i confini esterni (rete, file system, database).
La verità è: la maggior parte dei bug che ho visto in produzione avviene ai confini tra le unità, non al loro interno. Gli integration test intercettano questi bug.
Testare più componenti insieme#
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { ShoppingCart } from "@/components/ShoppingCart";
import { CartProvider } from "@/contexts/CartContext";
// Mock solo il livello API — tutto il resto è reale
vi.mock("@/lib/api", () => ({
checkout: vi.fn().mockResolvedValue({ orderId: "ORD-123" }),
}));
describe("Shopping Cart Flow", () => {
const renderCart = (initialItems = []) => {
return render(
<CartProvider initialItems={initialItems}>
<ShoppingCart />
</CartProvider>
);
};
it("displays item count and total", () => {
renderCart([
{ id: "1", name: "Keyboard", price: 79.99, quantity: 1 },
{ id: "2", name: "Mouse", price: 49.99, quantity: 2 },
]);
expect(screen.getByText("3 items")).toBeInTheDocument();
expect(screen.getByText("$179.97")).toBeInTheDocument();
});
it("completes checkout flow", async () => {
const user = userEvent.setup();
const { checkout } = await import("@/lib/api");
renderCart([
{ id: "1", name: "Keyboard", price: 79.99, quantity: 1 },
]);
await user.click(screen.getByRole("button", { name: /checkout/i }));
expect(checkout).toHaveBeenCalledWith({
items: [{ id: "1", quantity: 1 }],
});
expect(await screen.findByText(/order confirmed/i)).toBeInTheDocument();
});
});Coverage#
Eseguire la coverage#
vitest run --coverageHai bisogno di un coverage provider:
# V8 — più veloce, usa la coverage integrata di V8
npm install -D @vitest/coverage-v8
# Istanbul — più maturo, più opzioni di configurazione
npm install -D @vitest/coverage-istanbulConfiguralo nel tuo Vitest config:
export default defineConfig({
test: {
coverage: {
provider: "v8",
reporter: ["text", "html", "lcov"],
include: ["src/**/*.{ts,tsx}"],
exclude: [
"src/**/*.test.{ts,tsx}",
"src/**/*.spec.{ts,tsx}",
"src/test/**",
"src/**/*.d.ts",
"src/**/types.ts",
],
thresholds: {
statements: 80,
branches: 75,
functions: 80,
lines: 80,
},
},
},
});Cosa significa davvero la coverage#
La coverage ti dice quali righe di codice sono state eseguite durante i test. Tutto qui. Non ti dice se quelle righe sono state testate correttamente. Considera:
function divide(a: number, b: number): number {
return a / b;
}
it("divides numbers", () => {
divide(10, 2);
// Nessuna asserzione!
});Questo test dà il 100% di coverage della funzione divide. E non testa assolutamente niente.
La coverage è un indicatore negativo utile: bassa coverage significa che ci sono sicuramente percorsi non testati. Ma alta coverage non significa che il tuo codice sia ben testato.
Testare Next.js#
Mocking di next/navigation#
I componenti Next.js che usano useRouter, usePathname o useSearchParams hanno bisogno di mock:
import { vi } from "vitest";
vi.mock("next/navigation", () => ({
useRouter: () => ({
push: vi.fn(),
replace: vi.fn(),
back: vi.fn(),
prefetch: vi.fn(),
refresh: vi.fn(),
}),
usePathname: () => "/en/blog",
useSearchParams: () => new URLSearchParams("?page=1"),
useParams: () => ({ locale: "en" }),
}));Mocking di next-intl#
Per componenti internazionalizzati con next-intl:
vi.mock("next-intl", () => ({
useTranslations: () => (key: string) => key,
useLocale: () => "en",
}));Questo è l'approccio più semplice — le traduzioni restituiscono la chiave stessa, così t("hero.title") restituisce "hero.title". Nelle asserzioni, verifichi la chiave di traduzione piuttosto che la stringa tradotta. Questo rende i test indipendenti dalla lingua.
Testare i Route Handler#
I Route Handler di Next.js sono funzioni regolari che prendono una Request e restituiscono una Response. Sono semplici da testare:
import { GET, POST } from "@/app/api/users/route";
import { NextRequest } from "next/server";
describe("GET /api/users", () => {
it("returns users list", async () => {
const request = new NextRequest("http://localhost:3000/api/users");
const response = await GET(request);
const data = await response.json();
expect(response.status).toBe(200);
expect(data).toEqual(
expect.arrayContaining([
expect.objectContaining({ id: expect.any(Number), name: expect.any(String) }),
])
);
});
});Testare il middleware#
Il middleware di Next.js viene eseguito all'edge ed elabora ogni richiesta. Testalo come funzione:
import { middleware } from "@/middleware";
import { NextRequest } from "next/server";
function createRequest(path: string, headers: Record<string, string> = {}): NextRequest {
const url = new URL(path, "http://localhost:3000");
return new NextRequest(url, { headers });
}
describe("middleware", () => {
it("redirects unauthenticated users from protected routes", async () => {
const request = createRequest("/dashboard");
const response = await middleware(request);
expect(response.status).toBe(307);
expect(response.headers.get("location")).toContain("/login");
});
it("adds security headers", async () => {
const request = createRequest("/");
const response = await middleware(request);
expect(response.headers.get("x-frame-options")).toBe("DENY");
expect(response.headers.get("x-content-type-options")).toBe("nosniff");
});
});La filosofia del testing#
Testa il comportamento, non l'implementazione#
Questo principio è così importante che merita una sezione dedicata:
// Test di implementazione — fragile, si rompe con i refactor
it("calls setState with new count", () => {
const setStateSpy = vi.spyOn(React, "useState");
render(<Counter />);
fireEvent.click(screen.getByText("+"));
expect(setStateSpy).toHaveBeenCalledWith(expect.any(Function));
});
// Test di comportamento — resiliente, testa ciò che l'utente vede
it("increments the displayed count when plus button is clicked", async () => {
const user = userEvent.setup();
render(<Counter />);
expect(screen.getByText("Count: 0")).toBeInTheDocument();
await user.click(screen.getByRole("button", { name: "+" }));
expect(screen.getByText("Count: 1")).toBeInTheDocument();
});Il primo test si rompe se passi da useState a useReducer, anche se il componente funziona esattamente allo stesso modo. Il secondo test si rompe solo se il comportamento del componente cambia davvero.
Il test di cartina tornasole è semplice: puoi rifattorizzare l'implementazione senza cambiare il test? Se sì, stai testando il comportamento. Se no, stai testando l'implementazione.
Il Testing Trophy#
Kent C. Dodds ha proposto il "Testing Trophy" come alternativa alla tradizionale piramide dei test:
╭─────────╮
│ E2E │ Pochi — costosi, lenti, alta fiducia
├─────────┤
│ │
│ Integr. │ La maggior parte — buon rapporto fiducia/costo
│ │
├─────────┤
│ Unit │ Alcuni — veloci, focalizzati, basso costo
├─────────┤
│ Static │ Sempre — TypeScript, ESLint
╰─────────╯
Gli integration test sono il punto dolce. Verificano che componenti, hook e context lavorino insieme. Danno la maggior fiducia per test scritto.
Il testing come feedback di design#
Il codice difficile da testare è solitamente codice mal progettato. Se devi mockare cinque cose per testare una funzione, quella funzione ha troppe dipendenze. Se non riesci a renderizzare un componente senza configurare elaborati context provider, il componente è troppo accoppiato al suo ambiente.
// Difficile da testare — la funzione fa troppe cose
async function processOrder(orderId: string) {
const order = await db.orders.findById(orderId);
const user = await db.users.findById(order.userId);
const inventory = await checkInventory(order.items);
// ... molti effetti collaterali
}
// Più facile da testare — responsabilità separate
function determineOrderAction(
inventory: InventoryResult,
payment: PaymentResult
): OrderAction {
if (!inventory.available) return { type: "out-of-stock", items: inventory.unavailable };
if (!payment.success) return { type: "payment-failed", error: payment.error };
return { type: "confirmed" };
}La seconda versione è una funzione pura. Puoi testare ogni combinazione di inventario e risultati di pagamento senza mockare database, provider di pagamento o servizi email.
Scrivi test di cui ti fidi#
La cosa peggiore che può succedere a una suite di test non è che abbia delle lacune. È che le persone smettano di fidarsi. Una suite di test con alcuni test flaky che falliscono casualmente nella CI insegna al team a ignorare le build rosse. Una volta che succede, la suite di test è peggio che inutile.
Se un test fallisce in modo intermittente, aggiustalo o cancellalo. Se un test è lento, velocizzalo o spostalo in una suite di test lenti separata. Se un test si rompe a ogni modifica non correlata, riscrivilo per testare il comportamento invece dell'implementazione.
L'obiettivo è una suite di test dove ogni fallimento significa che qualcosa di reale è rotto. Quando gli sviluppatori si fidano dei test, li eseguono prima di ogni commit. Quando non si fidano, li bypassano con --no-verify e fanno deploy a dita incrociate.
Costruisci una suite di test su cui scommetteresti il tuo weekend. Niente di meno vale la pena di mantenere.