Writing tests
Write tests with Bun's Jest-compatible API, including async tests, timeouts, and test modifiers
Define tests with a Jest-like API imported from the built-in bun:test module. Long term, Bun aims for complete Jest compatibility.
Basic Usage#
To define a test:
import { expect, test } from "bun:test";
test("2 + 2", () => {
expect(2 + 2).toBe(4);
});Grouping Tests#
Group tests into suites with describe.
import { expect, test, describe } from "bun:test";
describe("arithmetic", () => {
test("2 + 2", () => {
expect(2 + 2).toBe(4);
});
test("2 * 2", () => {
expect(2 * 2).toBe(4);
});
});Async Tests#
Tests can be async.
import { expect, test } from "bun:test";
test("2 * 2", async () => {
const result = await Promise.resolve(2 * 2);
expect(result).toEqual(4);
});Alternatively, use the done callback to signal completion. If your test function takes a done parameter, you must call it or the test hangs.
import { expect, test } from "bun:test";
test("2 * 2", done => {
Promise.resolve(2 * 2).then(result => {
expect(result).toEqual(4);
done();
});
});Timeouts#
Optionally specify a per-test timeout in milliseconds by passing a number as the third argument to test.
import { expect, test } from "bun:test";
test("wat", async () => {
const data = await slowOperation();
expect(data).toBe(42);
}, 500); // test must run in <500msIn bun:test, a timeout throws an uncatchable exception to force the test to stop running and fail. Bun also kills any child processes spawned in the test, so they don't linger as zombie processes.
The default timeout for each test is 5000ms (5 seconds) unless you override it with this timeout option or jest.setTimeout().
Retries and Repeats#
test.retry#
Use the retry option to automatically retry a flaky test when it fails. The test passes if it succeeds within the specified number of attempts.
import { expect, test } from "bun:test";
test(
"flaky network request",
async () => {
const response = await fetch("https://example.com/api");
expect(response.ok).toBe(true);
},
{ retry: 3 }, // Retry up to 3 times if the test fails
);test.repeats#
Use the repeats option to run a test multiple times regardless of pass/fail status; the test fails if any iteration fails. Use it to detect flaky tests or for stress testing. repeats: N runs the test N+1 times total (1 initial run + N repeats).
import { expect, test } from "bun:test";
test(
"ensure test is stable",
() => {
expect(Math.random()).toBeLessThan(1);
},
{ repeats: 20 }, // Runs 21 times total (1 initial + 20 repeats)
);retry and repeats on the same test.🧟 Zombie Process Killer#
When a test times out, Bun kills any still-running processes that the test spawned with Bun.spawn, Bun.spawnSync, or node:child_process, and logs a message to the console. This prevents zombie processes from lingering after timed-out tests.
Test Modifiers#
test.skip#
Skip individual tests with test.skip. Bun does not run these tests.
import { expect, test } from "bun:test";
test.skip("wat", () => {
// TODO: fix this
expect(0.1 + 0.2).toEqual(0.3);
});test.todo#
Mark a test as a todo with test.todo. Bun does not run these tests.
import { expect, test } from "bun:test";
test.todo("fix this", () => {
myTestFunction();
});To run todo tests and find any that pass, use bun test --todo. Once myTestFunction() works, Bun reports the test above as a failure.
bun test --todomath.test.ts:
✗ fix this
^ this test is marked as todo but passes. Remove `.todo` if tested behavior now works
0 pass
1 failWith this flag, failing todo tests do not cause an error, but Bun marks todo tests that pass as failing so you can remove the todo mark or fix the test.
test.only#
To run a particular test or suite of tests, use test.only() or describe.only().
import { test, describe } from "bun:test";
test("test #1", () => {
// does not run
});
test.only("test #2", () => {
// runs
});
describe.only("only", () => {
test("test #3", () => {
// runs
});
});The following command runs only tests #2 and #3.
bun test --onlytest.if#
To run a test conditionally, use test.if(). The test runs if the condition is truthy. Use it for tests that should only run on a specific architecture or operating system.
test.if(Math.random() > 0.5)("runs half the time", () => {
// ...
});
const macOS = process.platform === "darwin";
test.if(macOS)("runs on macOS", () => {
// runs if macOS
});test.runIf() is an alias for test.if(), for Vitest compatibility.
test.skipIf#
To instead skip a test based on some condition, use test.skipIf() or describe.skipIf().
const macOS = process.platform === "darwin";
test.skipIf(macOS)("runs on non-macOS", () => {
// runs if *not* macOS
});test.todoIf#
To mark the test as TODO instead, use test.todoIf() or describe.todoIf(). The choice between skipIf and todoIf signals intent: "invalid for this target" versus "planned but not implemented yet."
const macOS = process.platform === "darwin";
// TODO: we've only implemented this for Linux so far.
test.todoIf(macOS)("runs on posix", () => {
// runs if *not* macOS
});test.failing#
Use test.failing() when you know a test is failing but you want to track it and be notified when it starts passing. This inverts the test result:
- A failing test marked with
.failing()passes - A passing test marked with
.failing()fails, with a message that it now passes and should be fixed
// This will pass because the test is failing as expected
test.failing("math is broken", () => {
expect(0.1 + 0.2).toBe(0.3); // fails due to floating point precision
});
// This will fail with a message that the test is now passing
test.failing("fixed bug", () => {
expect(1 + 1).toBe(2); // passes, but we expected it to fail
});Use it to track known bugs you plan to fix later, or for test-driven development.
test.fails() is Vitest's version. It passes if any part of the test fails: the callback, a beforeEach or afterEach hook, the timeout, or expect.assertions(). test.failing() only counts an error from the test callback.
Neither one counts an uncaught exception or an unhandled rejection, because it may have come from an earlier test. Bun reports it as an unhandled error between tests.
Combining modifiers#
You can chain modifiers in any order. Repeating a modifier, as in test.skip.skip, has no effect. skip wins over todo, and todo wins over failing.
test.skipIf(isWindows).skipIf(isCI)("skipped on Windows and in CI", () => {});
test.skipIf(isWindows).failing("skipped on Windows, expected to fail elsewhere", () => {
expect(0.1 + 0.2).toBe(0.3);
});Conditional Tests for Describe Blocks#
The conditional modifiers .if(), .skipIf(), and .todoIf() also work on describe blocks, affecting all tests in the suite:
const isMacOS = process.platform === "darwin";
// Only runs the entire suite on macOS
describe.if(isMacOS)("macOS-specific features", () => {
test("feature A", () => {
// only runs on macOS
});
test("feature B", () => {
// only runs on macOS
});
});
// Skips the entire suite on Windows
describe.skipIf(process.platform === "win32")("Unix features", () => {
test("feature C", () => {
// skipped on Windows
});
});
// Marks the entire suite as TODO on Linux
describe.todoIf(process.platform === "linux")("Upcoming Linux support", () => {
test("feature D", () => {
// marked as TODO on Linux
});
});Parametrized Tests#
test.each and describe.each#
To run the same test with multiple sets of data, use test.each. This creates a parametrized test that runs once for each test case provided.
const cases = [
[1, 2, 3],
[3, 4, 7],
];
test.each(cases)("%p + %p should be %p", (a, b, expected) => {
expect(a + b).toBe(expected);
});describe.each creates a parametrized suite that runs once for each test case:
describe.each([
[1, 2, 3],
[3, 4, 7],
])("add(%i, %i)", (a, b, expected) => {
test(`returns ${expected}`, () => {
expect(a + b).toBe(expected);
});
test(`sum is greater than each value`, () => {
expect(a + b).toBeGreaterThan(a);
expect(a + b).toBeGreaterThan(b);
});
});Argument Passing#
How Bun passes arguments to your test function depends on the structure of your test cases:
- If a table row is an array (like
[1, 2, 3]), Bun passes each element as an individual argument - If a row is not an array (like an object), Bun passes it as a single argument
// Array items passed as individual arguments
test.each([
[1, 2, 3],
[4, 5, 9],
])("add(%i, %i) = %i", (a, b, expected) => {
expect(a + b).toBe(expected);
});
// Object items passed as a single 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);
});You can also write the table as a tagged template. The first line names the columns, and each row is passed as an object:
test.each`
a | b | expected
${1} | ${2} | ${3}
${4} | ${5} | ${9}
`("add($a, $b) = $expected", ({ a, b, expected }) => {
expect(a + b).toBe(expected);
});test.for and describe.for#
test.for works like test.each, but it doesn't spread array rows. The row is always the first argument, and the second argument is the test context.
test.for([
[1, 2, 3],
[4, 5, 9],
])("add(%i, %i) = %i", ([a, b, expected], { expect }) => {
expect(a + b).toBe(expected);
});Format Specifiers#
Use these specifiers to format the test title:
| Specifier | Description |
|---|---|
%p | pretty-format |
%s | String |
%d | Number |
%i | Integer (parseInt) |
%f | Floating point (parseFloat) |
%j | JSON |
%o, %O | Object |
%# | Index of the test case |
%$ | Number of the test case (index + 1) |
%% | Single percent sign (%) |
Each specifier accepts any value and converts it the way util.format does. For example, %s prints 1 for 1 and null for null. Objects and arrays are printed on one line.
When a row is an object, $name and $user.name print its properties, and $# prints the test case's index. If the row has no name property, $name is left as is.
Examples#
// Basic specifiers
test.each([
["hello", 123],
["world", 456],
])("string: %s, number: %i", (str, num) => {
// "string: hello, number: 123"
// "string: world, number: 456"
});
// %p for pretty-format output
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 }'
});
// %# for index
test.each(["apple", "banana"])("fruit #%# is %s", fruit => {
// "fruit #0 is apple"
// "fruit #1 is banana"
});Fixtures#
test.extend() returns a new test function with fixtures. A fixture is a value that's set up before a test and torn down after it. Tests get fixtures by destructuring the callback's first parameter.
import { test as base, expect } from "bun:test";
const test = base.extend<{ server: Bun.Server<undefined>; url: string }>({
server: async ({}, use) => {
const server = Bun.serve({ port: 0, fetch: () => new Response("ok") }); // setup
await use(server); // the test runs here
await server.stop(); // teardown
},
url: async ({ server }, use) => {
await use(server.url.href);
},
});
test("responds", async ({ url }) => {
expect(await (await fetch(url)).text()).toBe("ok");
});- By default, a fixture is only set up for tests that destructure it, so the first parameter must be an object destructuring pattern. A fixture declares the fixtures it depends on the same way.
test.beforeEach()andtest.afterEach()on the extendedtestreceive fixtures, like a test does.beforeEachandafterEachimported from"bun:test"don't. Their parameter is adonecallback.- An
aroundEachhook receives fixtures in its second parameter:aroundEach(async (runTest, { server }) => {}). - Fixtures are torn down in reverse order, after the
afterEachhooks. A fixture that anaroundEachhook destructures is torn down after the hook finishes. - A fixture that isn't a function is passed as is.
- You can call
test.extend()on an extendedtest. A fixture that replaces one with the same name can destructure the one it replaces.
| Option | Description |
|---|---|
[fixture, { auto: true }] | Sets up the fixture for every test, even tests that don't destructure it |
[fixture, { scope: "file" }] | Sets up the fixture once, for the first test that needs it, and tears it down after the file's last test |
test.override({ name: fixture }) | Replaces fixtures in the describe block it's called in |
test.beforeAll(({ name }) => {}) | beforeAll, afterAll and aroundAll hooks that receive the scope: "file" fixtures |
Test Context#
The test context is the object you destructure fixtures from. Each test has one, and its hooks receive the same object.
| Property | Description |
|---|---|
task | The running test: name, fullName, suite, file.filepath, meta, result.state, result.errors, ... |
expect | An expect whose assertion counts and snapshots belong to this test, even in concurrent tests |
skip(note?), skip(condition, note?) | Stops the test and reports it as skipped |
onTestFinished(fn), onTestFailed(fn) | Runs fn after the test's afterEach hooks and fixture teardown. onTestFailed only runs it if the test failed |
signal | An AbortSignal that's aborted when the test times out |
annotate(message, type?, attachment?) | Adds an entry to task.annotations |
As in Vitest, the context isn't a done callback. Calling it throws an error.
Importing from vitest#
In bun test, "vitest" is a built-in module, like "bun:test" and "@jest/globals". Each module behaves like the runner it's named after, so a test file or helper module keeps the behavior it was written for:
"bun:test", "@jest/globals" | "vitest" | |
|---|---|---|
| Test or hook callback's parameter | a done callback that Bun waits for | the test context |
test.each callback's extra parameter | a done callback | undefined |
Values in a test.each title | printed in full: { a: "x" } | printed as in Vitest: { a: 'x' }, [ 0, 1, …(118) ] |
beforeAll and afterAll callback parameters | (done) | ({}, suite) |
afterEach and afterAll hooks in the same scope | run in registration order | run in reverse registration order |
Function returned by beforeEach or beforeAll | ignored | runs after the matching afterEach / afterAll hooks |
test(name, { skip, only, todo, fails, concurrent }, fn) | these options are ignored | these options work like the modifiers |
describe(name, options, fn) | options are ignored | options are the defaults for the tests inside |
test(name) without a callback | an error | a todo test |
| Mock functions before each test | left alone | cleared (clearMocks) |
| Default hook timeout | same as a test's | 10 seconds, or --timeout if it's longer |
beforeAll in a scope with only skipped tests | runs | doesn't run |
| Hook registered while a test runs | an error, or runs after that test | never runs |
.not.toMatchObject() with a value that isn't an object | an error | passes |
toHaveProperty("a.b") on { "a.b": 1 } | fails: "a.b" is always a path | passes: an own key with that name comes before the path |
toHaveProperty() on undefined or null | fails, so it passes with .not | throws a TypeError, with .not too |
toContain() on a string with a value that isn't one | an error | converts the value to a string |
.resolves.toThrow() on a promise resolved to an error | passes | fails |
.not.not | cancels out | still negates |
vi.useFakeTimers() | leaves setImmediate real | fakes setImmediate (list) |
performance while fake timers are on | the same object, on the fake clock | a new object; the one it replaces still reads the real clock |
"vitest" also exports suite, vitest, onTestFailed, assertType and TestRunner.getCurrentTest(), which returns the running test's task. It exports inject too, which always returns undefined, because bun test has no provide option or global setup file to provide a value.
import { assert, should } from "vitest" imports them from the chai package in your node_modules, which Vitest depends on. If chai isn't there, as with pnpm, Bun looks next to the installed vitest. They aren't available through import * as, import() or a re-export of "vitest".
In a file that imports from "vitest", the globals (test, describe, beforeEach, ...) come from "vitest" too. Set globals = "vitest" in bunfig.toml to do that for every file.
Types#
TypeScript reads the types for "vitest" from the vitest package. Once you uninstall it, use the ones in bun-types:
{
"compilerOptions": {
"types": ["bun-types/vitest"]
}
}types entry | Declares |
|---|---|
bun-types/vitest | the "vitest" module |
bun-types/vitest-globals | the module and its globals. Use it in place of vitest/globals. |
A directive in one file of your project does the same as the types entry:
/// <reference types="bun-types/vitest" />- Neither entry loads the rest of Bun's types, so they don't declare
Bunor change the type offetch. Add"bun"totypesif you want those too. - Load an entry only after you uninstall
vitest. The entry and the package declare the same module, and their declarations conflict. - The types follow Vitest 5, where
MatchersandAssertionhave two type parameters,RandT. TypeScript rejects adeclare module "vitest"block that declares them with Vitest 4's<T = any>. - The types declare what
bun testimplements. Vitest's other entry points, such asvitest/nodeandvitest/config, have no types, because Bun doesn't have those modules. assertandshouldget their types from@types/chai. Without it, they areany. TypeScript accepts them through any kind of import, but only a named import works at run time.inject()has the typeProvidedContext[key] | undefined, because it returnsundefined.
Promises#
.resolves and .rejects run the matcher on the value a promise resolves to or rejects with. They also accept a thenable or a function that returns a promise. The matcher returns a promise, so await it:
test("promises", async () => {
await expect(Promise.resolve(42)).resolves.toBe(42);
await expect(fetchUser("invalid-id")).rejects.toThrow("User not found");
await expect(() => fetchUser("invalid-id")).rejects.toThrow("User not found");
});If you don't await a matcher, the test still waits for it before ending, and a failure still fails that test. Outside a test, the file waits for the matcher, and a failure is reported as an error between tests. Code after the matcher doesn't wait for it, so await the matcher if that code closes something the promise needs.
In a "bun:test" test, a promise that has already settled is checked right away, and a failed matcher throws instead of returning a rejected promise. In a "vitest" test, the matcher always returns a promise.
These matchers also return a promise:
toThrow()on a function that returns a pending promise- A custom matcher that returns a promise
- A matcher that compares a pending promise with
expect.resolvesToorexpect.rejectsTo
Don't change the values being compared while one of these matchers waits. If they change, the matcher fails.
Soft Assertions#
expect.soft() doesn't throw when a matcher fails. The test keeps running and is marked as failed at the end:
test("response", async () => {
const response = await fetch(url);
expect.soft(response.status).toBe(200);
expect.soft(response.headers.get("content-type")).toBe("text/html"); // checked even if the status is wrong
});expect.soft() works with .not, .resolves, .rejects, and custom matchers. Outside a test, or in a concurrent test after its first await, a failed matcher throws as usual.
Polling#
expect.poll() calls a function, awaits its result, and runs the matcher on it. It retries until the matcher passes. The matcher returns a promise:
test("the queue drains", async () => {
await expect.poll(() => queue.length).toBe(0);
await expect.poll(() => fetchStatus(), { interval: 10, timeout: 500, message: "status" }).toBe("ready");
});| Option | Default | Description |
|---|---|---|
interval | 50 | Milliseconds between attempts |
timeout | 1000 | Milliseconds before the promise rejects with the last failure |
message | Custom message shown if the matcher fails, as in expect(x, msg) |
intervalandtimeoutare measured in real time, even with fake timers. Each attempt also advances the fake clock byinterval.expect.poll()doesn't support.resolves,.rejects,toThrow(), or the snapshot matchers. Usevi.waitFor()for those.
Assertion Counting#
Bun supports verifying that a specific number of assertions were called during a test:
expect.hasAssertions()#
Use expect.hasAssertions() to verify that at least one assertion is called during a test:
test("async work calls assertions", async () => {
expect.hasAssertions(); // Will fail if no assertions are called
const data = await fetchData();
expect(data).toBeDefined();
});expect.hasAssertions() is especially useful in async tests, to make sure your assertions run.
expect.assertions(count)#
Use expect.assertions(count) to verify that a specific number of assertions are called during a test:
test("exactly two assertions", () => {
expect.assertions(2); // Will fail if not exactly 2 assertions are called
expect(1 + 1).toBe(2);
expect("hello").toContain("ell");
});expect.assertions(count) helps ensure all your assertions run, especially in complex async code with multiple code paths.
Custom Equality#
Use expect.addEqualityTesters() to teach toEqual(), toStrictEqual(), toMatchObject(), toHaveBeenCalledWith(), and the other deep-equality matchers how to compare your own types. A tester returns true or false, or undefined if it doesn't handle the two values.
function areVolumesEqual(a: unknown, b: unknown) {
if (a instanceof Volume && b instanceof Volume) return a.equals(b);
return undefined;
}
expect.addEqualityTesters([areVolumesEqual]);
test("1 L is 1000 mL", () => {
expect({ size: new Volume(1, "L") }).toEqual({ size: new Volume(1000, "mL") });
});A tester registered in a test file applies until that file ends. A tester registered in a preload script, or in a module that test files import, applies to every file after it.
To compare nested values inside a tester, call this.equals(a, b, customTesters). customTesters is the third argument the tester receives.
Test State#
expect.getState() returns information about the running test. Custom matchers can read the properties below from this.
describe("math", () => {
test("adds", () => {
const { currentTestName, testPath, assertionCalls } = expect.getState();
console.log(currentTestName); // "math adds"
});
});| Property | Description |
|---|---|
currentTestName | The describe names and the test name, joined by a space, or by > in a "vitest" test |
testPath | The test file's absolute path |
assertionCalls | Number of assertions the test has made |
expectedAssertionsNumber | The count passed to expect.assertions(), or null |
isExpectingAssertions | true after expect.hasAssertions(), unless the test also calls expect.assertions() |
expect.setState(state) merges your own properties into the object expect.getState() returns. It can also set the three assertion properties above. Custom matchers don't get your own properties on this.
Type Testing#
Bun includes expectTypeOf for testing TypeScript types, compatible with Vitest.
expectTypeOf#
These functions are no-ops at runtime. Run bun check or bun test --check to verify the type
assertions.
The expectTypeOf function provides type-level assertions that TypeScript's type checker verifies. To test your types:
- Write your type assertions using
expectTypeOf - Run
bun checkto check that your types are correct
import { expectTypeOf } from "bun:test";
// Basic type assertions
expectTypeOf<string>().toEqualTypeOf<string>();
expectTypeOf(123).toBeNumber();
expectTypeOf("hello").toBeString();
// Object type matching
expectTypeOf({ a: 1, b: "hello" }).toMatchObjectType<{ a: number }>();
// Function types
function greet(name: string): string {
return `Hello ${name}`;
}
expectTypeOf(greet).toBeFunction();
expectTypeOf(greet).parameters.toEqualTypeOf<[string]>();
expectTypeOf(greet).returns.toEqualTypeOf<string>();
// Array types
expectTypeOf([1, 2, 3]).items.toBeNumber();
// Promise types
expectTypeOf(Promise.resolve(42)).resolves.toBeNumber();For full documentation on expectTypeOf matchers, see the API Reference.
Matchers#
Bun implements the following matchers. Full Jest compatibility is planned; see the tracking issue.
Basic Matchers#
| Status | Matcher |
|---|---|
| ✅ | .not |
| ✅ | .toBe() |
| ✅ | .toEqual() |
| ✅ | .toBeNull() |
| ✅ | .toBeUndefined() |
| ✅ | .toBeNaN() |
| ✅ | .toBeDefined() |
| ✅ | .toBeFalsy() |
| ✅ | .toBeTruthy() |
| ✅ | .toStrictEqual() |
String and Array Matchers#
| Status | Matcher |
|---|---|
| ✅ | .toContain() |
| ✅ | .toHaveLength() |
| ✅ | .toMatch() |
| ✅ | .toContainEqual() |
| ✅ | .stringContaining() |
| ✅ | .stringMatching() |
| ✅ | .arrayContaining() |
expect.stringMatching() takes a regular expression. A string is the source of one, as in new RegExp(string). To look for a literal string, use expect.stringContaining().
Object Matchers#
| Status | Matcher |
|---|---|
| ✅ | .toHaveProperty() |
| ✅ | .toMatchObject() |
| ✅ | .toContainAllKeys() |
| ✅ | .toContainValue() |
| ✅ | .toContainValues() |
| ✅ | .toContainAllValues() |
| ✅ | .toContainAnyValues() |
| ✅ | .objectContaining() |
Number Matchers#
| Status | Matcher |
|---|---|
| ✅ | .toBeCloseTo() |
| ✅ | .closeTo() |
| ✅ | .toBeGreaterThan() |
| ✅ | .toBeGreaterThanOrEqual() |
| ✅ | .toBeLessThan() |
| ✅ | .toBeLessThanOrEqual() |
Function and Class Matchers#
| Status | Matcher |
|---|---|
| ✅ | .toThrow() |
| ✅ | .toBeInstanceOf() |
Promise Matchers#
| Status | Matcher |
|---|---|
| ✅ | .resolves() |
| ✅ | .rejects() |
Mock Function Matchers#
| Status | Matcher |
|---|---|
| ✅ | .toHaveBeenCalled() |
| ✅ | .toHaveBeenCalledTimes() |
| ✅ | .toHaveBeenCalledWith() |
| ✅ | .toHaveBeenLastCalledWith() |
| ✅ | .toHaveBeenNthCalledWith() |
| ✅ | .toHaveBeenCalledExactlyOnceWith() |
| ✅ | .toHaveBeenCalledBefore() |
| ✅ | .toHaveBeenCalledAfter() |
| ✅ | .toHaveReturned() |
| ✅ | .toHaveReturnedTimes() |
| ✅ | .toHaveReturnedWith() |
| ✅ | .toHaveLastReturnedWith() |
| ✅ | .toHaveNthReturnedWith() |
| ✅ | .toHaveResolved() |
| ✅ | .toHaveResolvedTimes() |
| ✅ | .toHaveResolvedWith() |
| ✅ | .toHaveLastResolvedWith() |
| ✅ | .toHaveNthResolvedWith() |
Snapshot Matchers#
| Status | Matcher |
|---|---|
| ✅ | .toMatchSnapshot() |
| ✅ | .toMatchInlineSnapshot() |
| ✅ | .toThrowErrorMatchingSnapshot() |
| ✅ | .toThrowErrorMatchingInlineSnapshot() |
| ✅ | .toMatchFileSnapshot() |
Utility Matchers#
| Status | Matcher |
|---|---|
| ✅ | .extend |
| ✅ | .anything() |
| ✅ | .any() |
| ✅ | .toSatisfy() |
| ✅ | .assertions() |
| ✅ | .hasAssertions() |
| ✅ | .addEqualityTesters() |
| ✅ | .getState() |
| ✅ | .setState() |
| ✅ | .addSnapshotSerializer() |
expect.toSatisfy(predicate) stands in for a value, like expect.any(). It matches any value that predicate returns a truthy value for:
const isOdd = (n: number) => n % 2 === 1;
test("the count is odd", () => {
expect({ count: 3 }).toEqual({ count: expect.toSatisfy(isOdd) });
expect({ count: 2 }).toEqual({ count: expect.not.toSatisfy(isOdd) });
});Like in Jest and Vitest, you can also use any object with an asymmetricMatch(value) method as a matcher:
const anyFunction = { asymmetricMatch: (value: unknown) => typeof value === "function" };
test("the callback is a function", () => {
expect({ callback: () => {} }).toEqual({ callback: anyFunction });
});Best Practices#
Use Descriptive Test Names#
// Good
test("should calculate total price including tax for multiple items", () => {
// test implementation
});
// Avoid
test("price calculation", () => {
// test implementation
});Group Related Tests#
describe("User authentication", () => {
describe("with valid credentials", () => {
test("should return user data", () => {
// test implementation
});
test("should set authentication token", () => {
// test implementation
});
});
describe("with invalid credentials", () => {
test("should throw authentication error", () => {
// test implementation
});
});
});Use Appropriate Matchers#
// Good: Use specific matchers
expect(users).toHaveLength(3);
expect(user.email).toContain("@");
expect(response.status).toBeGreaterThanOrEqual(200);
// Avoid: Using toBe for everything
expect(users.length === 3).toBe(true);
expect(user.email.includes("@")).toBe(true);
expect(response.status >= 200).toBe(true);Test Error Conditions#
test("should throw error for invalid input", () => {
expect(() => {
validateEmail("not-an-email");
}).toThrow("Invalid email format");
});
test("should handle async errors", async () => {
await expect(async () => {
await fetchUser("invalid-id");
}).rejects.toThrow("User not found");
});Use Setup and Teardown#
import { beforeEach, afterEach, test } from "bun:test";
let testUser;
beforeEach(() => {
testUser = createTestUser();
});
afterEach(() => {
cleanupTestUser(testUser);
});
test("should update user profile", () => {
// Use testUser in test
});