Skip to content

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 :

ts
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.

ts
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.

ts
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é.

ts
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.

ts
import { test } from "bun:test";

test("wat", async () => {
  const data = await slowOperation();
  expect(data).toBe(42);
}, 500); // le test doit s'exécuter en <500ms

Dans 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.

ts
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).

ts
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.

ts
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.

ts
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.

bash
bun test --todo
my.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() calls

Avec 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().

ts
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.

bash
bun test --only

La commande suivante exécutera uniquement les tests #1, #2 et #3.

bash
bun test

test.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.

ts
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().

ts
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é".

ts
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é)
ts
// 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 :

ts
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.

ts
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 :

ts
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
ts
// É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écificateurDescription
%ppretty-format
%sString
%dNumber
%iInteger
%fFloating point
%jJSON
%oObject
%#Index du cas de test
%%Signe pourcent unique (%)

Exemples

ts
// 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 :

ts
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 :

ts
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 :

  1. Écrivez vos assertions de type en utilisant expectTypeOf
  2. Exécutez bunx tsc --noEmit pour vérifier que vos types sont corrects
ts
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

StatutMatcher
.not
.toBe()
.toEqual()
.toBeNull()
.toBeUndefined()
.toBeNaN()
.toBeDefined()
.toBeFalsy()
.toBeTruthy()
.toStrictEqual()

Matchers de chaîne et tableau

StatutMatcher
.toContain()
.toHaveLength()
.toMatch()
.toContainEqual()
.stringContaining()
.stringMatching()
.arrayContaining()

Matchers d'objet

StatutMatcher
.toHaveProperty()
.toMatchObject()
.toContainAllKeys()
.toContainValue()
.toContainValues()
.toContainAllValues()
.toContainAnyValues()
.objectContaining()

Matchers de nombre

StatutMatcher
.toBeCloseTo()
.closeTo()
.toBeGreaterThan()
.toBeGreaterThanOrEqual()
.toBeLessThan()
.toBeLessThanOrEqual()

Matchers de fonction et classe

StatutMatcher
.toThrow()
.toBeInstanceOf()

Matchers de Promise

StatutMatcher
.resolves()
.rejects()

Matchers de fonction mock

StatutMatcher
.toHaveBeenCalled()
.toHaveBeenCalledTimes()
.toHaveBeenCalledWith()
.toHaveBeenLastCalledWith()
.toHaveBeenNthCalledWith()
.toHaveReturned()
.toHaveReturnedTimes()
.toHaveReturnedWith()
.toHaveLastReturnedWith()
.toHaveNthReturnedWith()

Matchers de snapshot

StatutMatcher
.toMatchSnapshot()
.toMatchInlineSnapshot()
.toThrowErrorMatchingSnapshot()
.toThrowErrorMatchingInlineSnapshot()

Matchers utilitaires

StatutMatcher
.extend
.anything()
.any()
.assertions()
.hasAssertions()

Pas encore implémenté

StatutMatcher
.addSnapshotSerializer()

Bonnes pratiques

Utiliser des noms de tests descriptifs

ts
// 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

ts
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

ts
// 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

ts
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

ts
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
});

Bun édité par www.bunjs.com.cn