Définissez des tests avec une API de type Jest importée du module intégré bun:test. À long terme, Bun vise une compatibilité complète avec Jest ; pour le moment, un ensemble limité de matchers expect est pris en charge.
Utilisation de base
Pour définir un test simple :
import { expect, test } from "bun:test";
test("2 + 2", () => {
expect(2 + 2).toBe(4);
});Grouper des tests
Les tests peuvent être regroupés en suites avec describe.
import { expect, test, describe } from "bun:test";
describe("arithmétique", () => {
test("2 + 2", () => {
expect(2 + 2).toBe(4);
});
test("2 * 2", () => {
expect(2 * 2).toBe(4);
});
});Tests async
Les tests peuvent être async.
import { expect, test } from "bun:test";
test("2 * 2", async () => {
const result = await Promise.resolve(2 * 2);
expect(result).toEqual(4);
});Alternativement, utilisez le callback done pour signaler la fin. Si vous incluez le callback done comme paramètre dans votre définition de test, vous devez l'appeler ou le test restera bloqué.
import { expect, test } from "bun:test";
test("2 * 2", done => {
Promise.resolve(2 * 2).then(result => {
expect(result).toEqual(4);
done();
});
});Timeouts
Spécifiez optionnellement un timeout par test en millisecondes en passant un nombre comme troisième argument à test.
import { test } from "bun:test";
test("wat", async () => {
const data = await slowOperation();
expect(data).toBe(42);
}, 500); // le test doit s'exécuter en <500msDans bun:test, les timeouts de test lancent une exception non attrapable pour forcer le test à s'arrêter et échouer. Nous tuons également tous les processus enfants qui ont été créés dans le test pour éviter de laisser des processus zombies en arrière-plan.
Le timeout par défaut pour chaque test est de 5000ms (5 secondes) s'il n'est pas remplacé par cette option de timeout ou jest.setDefaultTimeout().
Retries et repeats
test.retry
Utilisez l'option retry pour réessayer automatiquement un test s'il échoue. Le test réussit s'il réussit dans le nombre d'essais spécifié. Ceci est utile pour les tests instables qui peuvent échouer de manière intermittente.
import { test } from "bun:test";
test(
"requête réseau instable",
async () => {
const response = await fetch("https://example.com/api");
expect(response.ok).toBe(true);
},
{ retry: 3 }, // Réessayer jusqu'à 3 fois si le test échoue
);test.repeats
Utilisez l'option repeats pour exécuter un test plusieurs fois quel que soit le statut réussite/échec. Le test échoue si une itération échoue. Ceci est utile pour détecter les tests instables ou les tests de stress. Notez que repeats: N exécute le test N+1 fois au total (1 exécution initiale + N repeats).
import { test } from "bun:test";
test(
"s'assurer que le test est stable",
() => {
expect(Math.random()).toBeLessThan(1);
},
{ repeats: 20 }, // S'exécute 21 fois au total (1 initial + 20 repeats)
);NOTE
Vous ne pouvez pas utiliser à la fois `retry` et `repeats` sur le même test.🧟 Tueur de processus zombies
Lorsqu'un test dépasse le timeout et que les processus créés dans le test via Bun.spawn, Bun.spawnSync ou node:child_process ne sont pas tués, ils seront automatiquement tués et un message sera affiché dans la console. Cela empêche les processus zombies de traîner en arrière-plan après les tests ayant dépassé le timeout.
Modificateurs de test
test.skip
Ignorez des tests individuels avec test.skip. Ces tests ne seront pas exécutés.
import { expect, test } from "bun:test";
test.skip("wat", () => {
// TODO : corriger ceci
expect(0.1 + 0.2).toEqual(0.3);
});test.todo
Marquez un test comme todo avec test.todo. Ces tests ne seront pas exécutés.
import { expect, test } from "bun:test";
test.todo("corriger ceci", () => {
myTestFunction();
});Pour exécuter les tests todo et trouver ceux qui réussissent, utilisez bun test --todo.
bun test --todomy.test.ts:
✗ fonctionnalité non implémentée
^ ce test est marqué comme todo mais réussit. Supprimez `.todo` ou vérifiez que le test est correct.
0 pass
1 fail
1 expect() callsAvec ce drapeau, les tests todo qui échouent ne causeront pas d'erreur, mais les tests todo qui réussissent seront marqués comme échouant afin que vous puissiez supprimer la marque todo ou corriger le test.
test.only
Pour exécuter un test ou une suite de tests particulier, utilisez test.only() ou describe.only().
import { test, describe } from "bun:test";
test("test #1", () => {
// ne s'exécute pas
});
test.only("test #2", () => {
// s'exécute
});
describe.only("only", () => {
test("test #3", () => {
// s'exécute
});
});La commande suivante exécutera uniquement les tests #2 et #3.
bun test --onlyLa commande suivante exécutera uniquement les tests #1, #2 et #3.
bun testtest.if
Pour exécuter un test conditionnellement, utilisez test.if(). Le test s'exécutera si la condition est truthy. Ceci est particulièrement utile pour les tests qui ne doivent s'exécuter que sur des architectures ou systèmes d'exploitation spécifiques.
test.if(Math.random() > 0.5)("s'exécute la moitié du temps", () => {
// ...
});
const macOS = process.platform === "darwin";
test.if(macOS)("s'exécute sur macOS", () => {
// s'exécute si macOS
});test.skipIf
Pour plutôt ignorer un test basé sur une condition, utilisez test.skipIf() ou describe.skipIf().
const macOS = process.platform === "darwin";
test.skipIf(macOS)("s'exécute sur non-macOS", () => {
// s'exécute si *pas* macOS
});test.todoIf
Si vous voulez plutôt marquer le test comme TODO, utilisez test.todoIf() ou describe.todoIf(). Choisir soigneusement skipIf ou todoIf peut montrer une différence entre, par exemple, l'intention de "invalide pour cette cible" et "prévu mais pas encore implémenté".
const macOS = process.platform === "darwin";
// TODO : nous n'avons implémenté ceci que pour Linux jusqu'à présent.
test.todoIf(macOS)("s'exécute sur posix", () => {
// s'exécute si *pas* macOS
});test.failing
Utilisez test.failing() lorsque vous savez qu'un test échoue actuellement mais que vous voulez le suivre et être notifié lorsqu'il commence à réussir. Cela inverse le résultat du test :
- Un test échouant marqué avec
.failing()réussira - Un test réussissant marqué avec
.failing()échouera (avec un message indiquant qu'il réussit maintenant et doit être corrigé)
// Cela réussira car le test échoue comme prévu
test.failing("math est cassé", () => {
expect(0.1 + 0.2).toBe(0.3); // échoue à cause de la précision des flottants
});
// Cela échouera avec un message indiquant que le test réussit maintenant
test.failing("bug corrigé", () => {
expect(1 + 1).toBe(2); // réussit, mais nous nous attendions à ce qu'il échoue
});Ceci est utile pour suivre les bugs connus que vous prévoyez de corriger plus tard, ou pour implémenter le développement piloté par les tests.
Tests conditionnels pour les blocs Describe
Les modificateurs conditionnels .if(), .skipIf() et .todoIf() peuvent également être appliqués aux blocs describe, affectant tous les tests dans la suite :
const isMacOS = process.platform === "darwin";
// S'exécute uniquement sur macOS
describe.if(isMacOS)("fonctionnalités spécifiques à macOS", () => {
test("fonctionnalité A", () => {
// s'exécute uniquement sur macOS
});
test("fonctionnalité B", () => {
// s'exécute uniquement sur macOS
});
});
// Ignore la suite entière sur Windows
describe.skipIf(process.platform === "win32")("fonctionnalités Unix", () => {
test("fonctionnalité C", () => {
// ignoré sur Windows
});
});
// Marque la suite entière comme TODO sur Linux
describe.todoIf(process.platform === "linux")("Support Linux à venir", () => {
test("fonctionnalité D", () => {
// marqué comme TODO sur Linux
});
});Tests paramétrés
test.each et describe.each
Pour exécuter le même test avec plusieurs ensembles de données, utilisez test.each. Cela crée un test paramétré qui s'exécute une fois pour chaque cas de test fourni.
const cases = [
[1, 2, 3],
[3, 4, 7],
];
test.each(cases)("%p + %p devrait être %p", (a, b, expected) => {
expect(a + b).toBe(expected);
});Vous pouvez également utiliser describe.each pour créer une suite paramétrée qui s'exécute une fois pour chaque cas de test :
describe.each([
[1, 2, 3],
[3, 4, 7],
])("add(%i, %i)", (a, b, expected) => {
test(`retourne ${expected}`, () => {
expect(a + b).toBe(expected);
});
test(`la somme est supérieure à chaque valeur`, () => {
expect(a + b).toBeGreaterThan(a);
expect(a + b).toBeGreaterThan(b);
});
});Passage d'arguments
La façon dont les arguments sont passés à votre fonction de test dépend de la structure de vos cas de test :
- Si une ligne de tableau est un tableau (comme
[1, 2, 3]), chaque élément est passé comme argument individuel - Si une ligne n'est pas un tableau (comme un objet), elle est passée comme un seul argument
// Éléments de tableau passés comme arguments individuels
test.each([
[1, 2, 3],
[4, 5, 9],
])("add(%i, %i) = %i", (a, b, expected) => {
expect(a + b).toBe(expected);
});
// Éléments d'objet passés comme un seul argument
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);
});Spécificateurs de format
Il y a plusieurs options disponibles pour formater le titre du test :
| Spécificateur | Description |
|---|---|
%p | pretty-format |
%s | String |
%d | Number |
%i | Integer |
%f | Floating point |
%j | JSON |
%o | Object |
%# | Index du cas de test |
%% | Signe pourcent unique (%) |
Exemples
// Spécificateurs de base
test.each([
["hello", 123],
["world", 456],
])("string: %s, number: %i", (str, num) => {
// "string: hello, number: 123"
// "string: world, number: 456"
});
// %p pour la sortie pretty-format
test.each([
[{ name: "Alice" }, { a: 1, b: 2 }],
[{ name: "Bob" }, { x: 5, y: 10 }],
])("user %p with data %p", (user, data) => {
// "user { name: 'Alice' } with data { a: 1, b: 2 }"
// "user { name: 'Bob' } with data { x: 5, y: 10 }"
});
// %# pour l'index
test.each(["apple", "banana"])("fruit #%# is %s", fruit => {
// "fruit #0 is apple"
// "fruit #1 is banana"
});Comptage d'assertions
Bun prend en charge la vérification qu'un nombre spécifique d'assertions a été appelé pendant un test :
expect.hasAssertions()
Utilisez expect.hasAssertions() pour vérifier qu'au moins une assertion est appelée pendant un test :
test("le travail async appelle les assertions", async () => {
expect.hasAssertions(); // Échouera si aucune assertion n'est appelée
const data = await fetchData();
expect(data).toBeDefined();
});Ceci est particulièrement utile pour les tests async pour s'assurer que vos assertions s'exécutent réellement.
expect.assertions(count)
Utilisez expect.assertions(count) pour vérifier qu'un nombre spécifique d'assertions est appelé pendant un test :
test("exactement deux assertions", () => {
expect.assertions(2); // Échouera si exactement 2 assertions ne sont pas appelées
expect(1 + 1).toBe(2);
expect("hello").toContain("ell");
});Cela aide à s'assurer que toutes vos assertions s'exécutent, en particulier dans le code async complexe avec plusieurs chemins de code.
Tests de type
Bun inclut expectTypeOf pour tester les types TypeScript, compatible avec Vitest.
expectTypeOf
La fonction expectTypeOf fournit des assertions de niveau type qui sont vérifiées par le vérificateur de type de TypeScript. Pour tester vos types :
- Écrivez vos assertions de type en utilisant
expectTypeOf - Exécutez
bunx tsc --noEmitpour vérifier que vos types sont corrects
import { expectTypeOf } from "bun:test";
// Assertions de type de base
expectTypeOf<string>().toEqualTypeOf<string>();
expectTypeOf(123).toBeNumber();
expectTypeOf("hello").toBeString();
// Correspondance de type d'objet
expectTypeOf({ a: 1, b: "hello" }).toMatchObjectType<{ a: number }>();
// Types de fonction
function greet(name: string): string {
return `Hello ${name}`;
}
expectTypeOf(greet).toBeFunction();
expectTypeOf(greet).parameters.toEqualTypeOf<[string]>();
expectTypeOf(greet).returns.toEqualTypeOf<string>();
// Types de tableau
expectTypeOf([1, 2, 3]).items.toBeNumber();
// Types de Promise
expectTypeOf(Promise.resolve(42)).resolves.toBeNumber();Pour la documentation complète sur les matchers expectTypeOf, consultez la Référence API.
Matchers
Bun implémente les matchers suivants. La compatibilité complète avec Jest est prévue ; suivez les progrès ici.
Matchers de base
| Statut | Matcher |
|---|---|
| ✅ | .not |
| ✅ | .toBe() |
| ✅ | .toEqual() |
| ✅ | .toBeNull() |
| ✅ | .toBeUndefined() |
| ✅ | .toBeNaN() |
| ✅ | .toBeDefined() |
| ✅ | .toBeFalsy() |
| ✅ | .toBeTruthy() |
| ✅ | .toStrictEqual() |
Matchers de chaîne et tableau
| Statut | Matcher |
|---|---|
| ✅ | .toContain() |
| ✅ | .toHaveLength() |
| ✅ | .toMatch() |
| ✅ | .toContainEqual() |
| ✅ | .stringContaining() |
| ✅ | .stringMatching() |
| ✅ | .arrayContaining() |
Matchers d'objet
| Statut | Matcher |
|---|---|
| ✅ | .toHaveProperty() |
| ✅ | .toMatchObject() |
| ✅ | .toContainAllKeys() |
| ✅ | .toContainValue() |
| ✅ | .toContainValues() |
| ✅ | .toContainAllValues() |
| ✅ | .toContainAnyValues() |
| ✅ | .objectContaining() |
Matchers de nombre
| Statut | Matcher |
|---|---|
| ✅ | .toBeCloseTo() |
| ✅ | .closeTo() |
| ✅ | .toBeGreaterThan() |
| ✅ | .toBeGreaterThanOrEqual() |
| ✅ | .toBeLessThan() |
| ✅ | .toBeLessThanOrEqual() |
Matchers de fonction et classe
| Statut | Matcher |
|---|---|
| ✅ | .toThrow() |
| ✅ | .toBeInstanceOf() |
Matchers de Promise
| Statut | Matcher |
|---|---|
| ✅ | .resolves() |
| ✅ | .rejects() |
Matchers de fonction mock
| Statut | Matcher |
|---|---|
| ✅ | .toHaveBeenCalled() |
| ✅ | .toHaveBeenCalledTimes() |
| ✅ | .toHaveBeenCalledWith() |
| ✅ | .toHaveBeenLastCalledWith() |
| ✅ | .toHaveBeenNthCalledWith() |
| ✅ | .toHaveReturned() |
| ✅ | .toHaveReturnedTimes() |
| ✅ | .toHaveReturnedWith() |
| ✅ | .toHaveLastReturnedWith() |
| ✅ | .toHaveNthReturnedWith() |
Matchers de snapshot
| Statut | Matcher |
|---|---|
| ✅ | .toMatchSnapshot() |
| ✅ | .toMatchInlineSnapshot() |
| ✅ | .toThrowErrorMatchingSnapshot() |
| ✅ | .toThrowErrorMatchingInlineSnapshot() |
Matchers utilitaires
| Statut | Matcher |
|---|---|
| ✅ | .extend |
| ✅ | .anything() |
| ✅ | .any() |
| ✅ | .assertions() |
| ✅ | .hasAssertions() |
Pas encore implémenté
| Statut | Matcher |
|---|---|
| ❌ | .addSnapshotSerializer() |
Bonnes pratiques
Utiliser des noms de tests descriptifs
// Bien
test("devrait calculer le prix total incluant la taxe pour plusieurs articles", () => {
// implémentation du test
});
// Éviter
test("calcul de prix", () => {
// implémentation du test
});Grouper les tests liés
describe("Authentification utilisateur", () => {
describe("avec des identifiants valides", () => {
test("devrait retourner les données utilisateur", () => {
// implémentation du test
});
test("devrait définir le jeton d'authentification", () => {
// implémentation du test
});
});
describe("avec des identifiants invalides", () => {
test("devrait lancer une erreur d'authentification", () => {
// implémentation du test
});
});
});Utiliser des matcheurs appropriés
// Bien : Utiliser des matcheurs spécifiques
expect(users).toHaveLength(3);
expect(user.email).toContain("@");
expect(response.status).toBeGreaterThanOrEqual(200);
// Éviter : Utiliser toBe pour tout
expect(users.length === 3).toBe(true);
expect(user.email.includes("@")).toBe(true);
expect(response.status >= 200).toBe(true);Tester les conditions d'erreur
test("devrait lancer une erreur pour une entrée invalide", () => {
expect(() => {
validateEmail("not-an-email");
}).toThrow("Format d'email invalide");
});
test("devrait gérer les erreurs async", async () => {
await expect(async () => {
await fetchUser("invalid-id");
}).rejects.toThrow("Utilisateur non trouvé");
});Utiliser la configuration et le nettoyage
import { beforeEach, afterEach, test } from "bun:test";
let testUser;
beforeEach(() => {
testUser = createTestUser();
});
afterEach(() => {
cleanupTestUser(testUser);
});
test("devrait mettre à jour le profil utilisateur", () => {
// Utiliser testUser dans le test
});