Definisci i test con un'API simile a Jest importata dal modulo integrato bun:test. A lungo termine, Bun mira alla compatibilita completa con Jest; al momento, e supportato un insieme limitato di matcher expect.
Uso Base
Per definire un semplice test:
import { expect, test } from "bun:test";
test("2 + 2", () => {
expect(2 + 2).toBe(4);
});Raggruppare i Test
I test possono essere raggruppati in suite con describe.
import { expect, test, describe } from "bun:test";
describe("aritmetica", () => {
test("2 + 2", () => {
expect(2 + 2).toBe(4);
});
test("2 * 2", () => {
expect(2 * 2).toBe(4);
});
});Test Asincroni
I test possono essere asincroni.
import { expect, test } from "bun:test";
test("2 * 2", async () => {
const result = await Promise.resolve(2 * 2);
expect(result).toEqual(4);
});In alternativa, usa il callback done per segnalare il completamento. Se includi il callback done come parametro nella definizione del test, devi chiamarlo o il test rimarra in attesa.
import { expect, test } from "bun:test";
test("2 * 2", done => {
Promise.resolve(2 * 2).then(result => {
expect(result).toEqual(4);
done();
});
});Timeout
Opzionalmente specifica un timeout per test in millisecondi passando un numero come terzo argomento a test.
import { test } from "bun:test";
test("wat", async () => {
const data = await slowOperation();
expect(data).toBe(42);
}, 500); // il test deve essere eseguito in meno di 500msIn bun:test, i timeout dei test lanciano un'eccezione non catturabile per forzare il test a smettere di essere eseguito e fallire. Uccidiamo anche tutti i processi figli che sono stati spawnati nel test per evitare di lasciare dietro processi zombie in background.
Il timeout predefinito per ogni test e 5000ms (5 secondi) se non viene sovrascritto da questa opzione di timeout o jest.setDefaultTimeout().
Retry e Ripetizioni
test.retry
Usa l'opzione retry per ritentare automaticamente un test se fallisce. Il test passa se ha successo entro il numero di tentativi specificato. Questo e utile per test flaky che potrebbero fallire intermittenza.
import { test } from "bun:test";
test(
"richiesta di rete flaky",
async () => {
const response = await fetch("https://example.com/api");
expect(response.ok).toBe(true);
},
{ retry: 3 }, // Ritenta fino a 3 volte se il test fallisce
);test.repeats
Usa l'opzione repeats per eseguire un test piu volte indipendentemente dallo stato pass/fail. Il test fallisce se qualsiasi iterazione fallisce. Questo e utile per rilevare test flaky o per stress testing. Nota che repeats: N esegue il test N+1 volte in totale (1 esecuzione iniziale + N ripetizioni).
import { test } from "bun:test";
test(
"assicurati che il test sia stabile", () => {
expect(Math.random()).toBeLessThan(1);
},
{ repeats: 20 }, // Esegue 21 volte in totale (1 iniziale + 20 ripetizioni)
);NOTE
Non puoi usare sia `retry` che `repeats` sullo stesso test.Processi Zombie Killer
Quando un test va in timeout e i processi spawnati nel test tramite Bun.spawn, Bun.spawnSync o node:child_process non vengono uccisi, saranno automaticamente uccisi e un messaggio sara registrato sulla console. Questo impedisce ai processi zombie di rimanere in background dopo test che hanno raggiunto il timeout.
Modificatori di Test
test.skip
Salta singoli test con test.skip. Questi test non saranno eseguiti.
import { expect, test } from "bun:test";
test.skip("wat", () => {
// TODO: fix this
expect(0.1 + 0.2).toEqual(0.3);
});test.todo
Marca un test come todo con test.todo. Questi test non saranno eseguiti.
import { expect, test } from "bun:test";
test.todo("fix this", () => {
myTestFunction();
});Per eseguire i test todo e trovare qualcuno che sta passando, usa bun test --todo.
bun test --todomy.test.ts:
✗ unimplemented feature
^ this test is marked as todo but passes. Remove `.todo` or check that test is correct.
0 pass
1 fail
1 expect() callsCon questo flag, i test todo che falliscono non causeranno un errore, ma i test todo che passano saranno marcati come falliti in modo da poter rimuovere il marchio todo o correggere il test.
test.only
Per eseguire un particolare test o suite di test usa test.only() o describe.only().
import { test, describe } from "bun:test";
test("test #1", () => {
// non esegue
});
test.only("test #2", () => {
// esegue
});
describe.only("only", () => {
test("test #3", () => {
// esegue
});
});Il seguente comando eseguira solo i test #2 e #3.
bun test --onlyIl seguente comando eseguira solo i test #1, #2 e #3.
bun testtest.if
Per eseguire un test condizionalmente, usa test.if(). Il test eseguira se la condizione e truthy. Questo e particolarmente utile per test che dovrebbero essere eseguiti solo su architetture o sistemi operativi specifici.
test.if(Math.random() > 0.5)("runs half the time", () => {
// ...
});
const macOS = process.platform === "darwin";
test.if(macOS)("runs on macOS", () => {
// esegue se macOS
});test.skipIf
Per saltare un test in base a qualche condizione, usa test.skipIf() o describe.skipIf().
const macOS = process.platform === "darwin";
test.skipIf(macOS)("runs on non-macOS", () => {
// esegue se *non* macOS
});test.todoIf
Se invece vuoi marcare il test come TODO, usa test.todoIf() o describe.todoIf(). Scegliere attentamente tra skipIf o todoIf puo mostrare una differenza tra, ad esempio, l'intento di "non valido per questo target" e "pianificato ma non ancora implementato".
const macOS = process.platform === "darwin";
// TODO: abbiamo implementato questo solo per Linux finora.
test.todoIf(macOS)("runs on posix", () => {
// esegue se *non* macOS
});test.failing
Usa test.failing() quando sai che un test sta attualmente fallendo ma vuoi tracciarlo ed essere notificato quando inizia a passare. Questo inverte il risultato del test:
- Un test fallito marcato con
.failing()passera - Un test passato marcato con
.failing()fallira (con un messaggio che indica che ora sta passando e dovrebbe essere corretto)
// Questo passara perche il test sta fallendo come previsto
test.failing("la matematica e rotta", () => {
expect(0.1 + 0.2).toBe(0.3); // fallisce a causa della precisione in virgola mobile
});
// Questo fallira con un messaggio che il test ora sta passando
test.failing("bug corretto", () => {
expect(1 + 1).toBe(2); // passa, ma ci aspettavamo che fallisse
});Questo e utile per tracciare bug noti che pianifichi di correggere successivamente, o per implementare lo sviluppo guidato dai test.
Test Condizionali per Blocchi Describe
I modificatori condizionali .if(), .skipIf() e .todoIf() possono anche essere applicati ai blocchi describe, influenzando tutti i test all'interno della suite:
const isMacOS = process.platform === "darwin";
// Esegue l'intera suite solo su macOS
describe.if(isMacOS)("funzionalita specifiche di macOS", () => {
test("feature A", () => {
// esegue solo su macOS
});
test("feature B", () => {
// esegue solo su macOS
});
});
// Salta l'intera suite su Windows
describe.skipIf(process.platform === "win32")("funzionalita Unix", () => {
test("feature C", () => {
// saltato su Windows
});
});
// Marca l'intera suite come TODO su Linux
describe.todoIf(process.platform === "linux")("Supporto Linux imminente", () => {
test("feature D", () => {
// marcato come TODO su Linux
});
});Test Parametrizzati
test.each e describe.each
Per eseguire lo stesso test con piu set di dati, usa test.each. Questo crea un test parametrizzato che viene eseguito una volta per ogni caso di test fornito.
const cases = [
[1, 2, 3],
[3, 4, 7],
];
test.each(cases)("%p + %p dovrebbe essere %p", (a, b, expected) => {
expect(a + b).toBe(expected);
});Puoi anche usare describe.each per creare una suite parametrizzata che viene eseguita una volta per ogni caso di test:
describe.each([
[1, 2, 3],
[3, 4, 7],
])("add(%i, %i)", (a, b, expected) => {
test(`ritorna ${expected}`, () => {
expect(a + b).toBe(expected);
});
test(`la somma e maggiore di ogni valore`, () => {
expect(a + b).toBeGreaterThan(a);
expect(a + b).toBeGreaterThan(b);
});
});Passaggio degli Argomenti
Come gli argomenti vengono passati alla tua funzione di test dipende dalla struttura dei tuoi casi di test:
- Se una riga della tabella e un array (come
[1, 2, 3]), ogni elemento viene passato come argomento individuale - Se una riga non e un array (come un oggetto), viene passato come un singolo argomento
// Elementi dell'array passati come argomenti individuali
test.each([
[1, 2, 3],
[4, 5, 9],
])("add(%i, %i) = %i", (a, b, expected) => {
expect(a + b).toBe(expected);
});
// Elementi oggetto passati come singolo argomento
test.each([
{ a: 1, b: 2, expected: 3 },
{ a: 4, b: 5, expected: 9 },
])("add($a, $b) = $expected", data => {
expect(data.a + data.b).toBe(data.expected);
});Specificatori di Formato
Ci sono diverse opzioni disponibili per formattare il titolo del test:
| Specificatore | Descrizione |
|---|---|
%p | pretty-format |
%s | String |
%d | Number |
%i | Integer |
%f | Floating point |
%j | JSON |
%o | Object |
%# | Indice del caso di test |
%% | Singolo segno di percentuale (%) |
Esempi
// Specificatori base
test.each([
["hello", 123],
["world", 456],
])("stringa: %s, numero: %i", (str, num) => {
// "stringa: hello, numero: 123"
// "stringa: world, numero: 456"
});
// %p per output pretty-format
test.each([
[{ name: "Alice" }, { a: 1, b: 2 }],
[{ name: "Bob" }, { x: 5, y: 10 }],
])("utente %p con dati %p", (user, data) => {
// "utente { name: 'Alice' } con dati { a: 1, b: 2 }"
// "utente { name: 'Bob' } con dati { x: 5, y: 10 }"
});
// %# per indice
test.each(["apple", "banana"])("frutta #%# e %s", fruit => {
// "frutta #0 e apple"
// "frutta #1 e banana"
});Conteggio delle Asserzioni
Bun supporta la verifica che un numero specifico di asserzioni sia stato chiamato durante un test:
expect.hasAssertions()
Usa expect.hasAssertions() per verificare che almeno un'asserzione sia chiamata durante un test:
test("lavoro asincrono chiama asserzioni", async () => {
expect.hasAssertions(); // Fallira se nessuna asserzione viene chiamata
const data = await fetchData();
expect(data).toBeDefined();
});Questo e particolarmente utile per i test asincroni per assicurarti che le tue asserzioni siano effettivamente eseguite.
expect.assertions(count)
Usa expect.assertions(count) per verificare che un numero specifico di asserzioni sia chiamato durante un test:
test("esattamente due asserzioni", () => {
expect.assertions(2); // Fallira se non esattamente 2 asserzioni sono chiamate
expect(1 + 1).toBe(2);
expect("hello").toContain("ell");
});
Questo aiuta a garantire che tutte le tue asserzioni siano eseguite, specialmente nel codice asincrono complesso con piu percorsi di codice.
## Test di Tipo
Bun include `expectTypeOf` per testare i tipi TypeScript, compatibile con Vitest.
### expectTypeOf
<Warning>
Queste funzioni sono no-op a runtime - devi eseguire TypeScript separatamente per verificare i controlli di tipo.
</Warning>
La funzione `expectTypeOf` fornisce asserzioni a livello di tipo che sono controllate dal type checker di TypeScript. Per testare i tuoi tipi:
1. Scrivi le tue asserzioni di tipo usando `expectTypeOf`
2. Esegui `bunx tsc --noEmit` per verificare che i tuoi tipi siano corretti
```ts [example.test.ts]
import { expectTypeOf } from "bun:test";
// Asserzioni di tipo base
expectTypeOf<string>().toEqualTypeOf<string>();
expectTypeOf(123).toBeNumber();
expectTypeOf("hello").toBeString();
// Corrispondenza del tipo oggetto
expectTypeOf({ a: 1, b: "hello" }).toMatchObjectType<{ a: number }>();
// Tipi di funzione
function greet(name: string): string {
return `Hello ${name}`;
}
expectTypeOf(greet).toBeFunction();
expectTypeOf(greet).parameters.toEqualTypeOf<[string]>();
expectTypeOf(greet).returns.toEqualTypeOf<string>();
// Tipi array
expectTypeOf([1, 2, 3]).items.toBeNumber();
// Tipi Promise
expectTypeOf(Promise.resolve(42)).resolves.toBeNumber();Per la documentazione completa sui matcher di expectTypeOf, vedi il Riferimento API.
Matcher
Bun implementa i seguenti matcher. La piena compatibilita con Jest e in tabella di marcia; traccia i progressi qui.
Matcher Base
| Stato | Matcher |
|---|---|
| ✅ | .not |
| ✅ | .toBe() |
| ✅ | .toEqual() |
| ✅ | .toBeNull() |
| ✅ | .toBeUndefined() |
| ✅ | .toBeNaN() |
| ✅ | .toBeDefined() |
| ✅ | .toBeFalsy() |
| ✅ | .toBeTruthy() |
| ✅ | .toStrictEqual() |
Matcher per Stringhe e Array
| Stato | Matcher |
|---|---|
| ✅ | .toContain() |
| ✅ | .toHaveLength() |
| ✅ | .toMatch() |
| ✅ | .toContainEqual() |
| ✅ | .stringContaining() |
| ✅ | .stringMatching() |
| ✅ | .arrayContaining() |
Matcher per Oggetti
| Stato | Matcher |
|---|---|
| ✅ | .toHaveProperty() |
| ✅ | .toMatchObject() |
| ✅ | .toContainAllKeys() |
| ✅ | .toContainValue() |
| ✅ | .toContainValues() |
| ✅ | .toContainAllValues() |
| ✅ | .toContainAnyValues() |
| ✅ | .objectContaining() |
Matcher per Numeri
| Stato | Matcher |
|---|---|
| ✅ | .toBeCloseTo() |
| ✅ | .closeTo() |
| ✅ | .toBeGreaterThan() |
| ✅ | .toBeGreaterThanOrEqual() |
| ✅ | .toBeLessThan() |
| ✅ | .toBeLessThanOrEqual() |
Matcher per Funzioni e Classi
| Stato | Matcher |
|---|---|
| ✅ | .toThrow() |
| ✅ | .toBeInstanceOf() |
Matcher per Promise
| Stato | Matcher |
|---|---|
| ✅ | .resolves() |
| ✅ | .rejects() |
Matcher per Funzioni Mock
| Stato | Matcher |
|---|---|
| ✅ | .toHaveBeenCalled() |
| ✅ | .toHaveBeenCalledTimes() |
| ✅ | .toHaveBeenCalledWith() |
| ✅ | .toHaveBeenLastCalledWith() |
| ✅ | .toHaveBeenNthCalledWith() |
| ✅ | .toHaveReturned() |
| ✅ | .toHaveReturnedTimes() |
| ✅ | .toHaveReturnedWith() |
| ✅ | .toHaveLastReturnedWith() |
| ✅ | .toHaveNthReturnedWith() |
Matcher per Snapshot
| Stato | Matcher |
|---|---|
| ✅ | .toMatchSnapshot() |
| ✅ | .toMatchInlineSnapshot() |
| ✅ | .toThrowErrorMatchingSnapshot() |
| ✅ | .toThrowErrorMatchingInlineSnapshot() |
Matcher di Utilita
| Stato | Matcher |
|---|---|
| ✅ | .extend |
| ✅ | .anything() |
| ✅ | .any() |
| ✅ | .assertions() |
| ✅ | .hasAssertions() |
Non Ancora Implementato
| Stato | Matcher |
|---|---|
| ❌ | .addSnapshotSerializer() |
Best Practices
Usa Nomi Descrittivi per i Test
// Buono
test("dovrebbe calcolare il prezzo totale incluse le tasse per piu articoli", () => {
// implementazione del test
});
// Evita
test("calcolo prezzo", () => {
// implementazione del test
});Raggruppa Test Correlati
describe("Autenticazione utente", () => {
describe("con credenziali valide", () => {
test("dovrebbe ritornare i dati utente", () => {
// implementazione del test
});
test("dovrebbe impostare il token di autenticazione", () => {
// implementazione del test
});
});
describe("con credenziali non valide", () => {
test("dovrebbe lanciare errore di autenticazione", () => {
// implementazione del test
});
});
});Usa Matcher Appropriati
// Buono: usa matcher specifici
expect(users).toHaveLength(3);
expect(user.email).toContain("@");
expect(response.status).toBeGreaterThanOrEqual(200);
// Evita: usare toBe per tutto
expect(users.length === 3).toBe(true);
expect(user.email.includes("@")).toBe(true);
expect(response.status >= 200).toBe(true);Testa le Condizioni di Errore
test("dovrebbe lanciare errore per input non valido", () => {
expect(() => {
validateEmail("not-an-email");
}).toThrow("Formato email non valido");
});
test("dovrebbe gestire errori asincroni", async () => {
await expect(async () => {
await fetchUser("invalid-id");
}).rejects.toThrow("Utente non trovato");
});Usa Setup e Teardown
import { beforeEach, afterEach, test } from "bun:test";
let testUser;
beforeEach(() => {
testUser = createTestUser();
});
afterEach(() => {
cleanupTestUser(testUser);
});
test("dovrebbe aggiornare il profilo utente", () => {
// Usa testUser nel test
});