Test, build, and ship
Write tests with bun:test
Create fast TypeScript tests with Bun's built-in test runner and use focused assertions to describe behavior.
Bun includes a test runner. You don’t install Jest or Vitest, you don’t configure anything, and your tests are TypeScript files that run directly. The API is the familiar test() and expect() style, so if you’ve written Jest tests you already know it.
Let’s test a small function first. Create title.ts:
export function normalizeTitle(title: string) {
return title.trim().replaceAll(/\s+/g, ' ')
}
It trims a note title and collapses runs of whitespace into one space. Now create title.test.ts beside it:
import { expect, test } from 'bun:test'
import { normalizeTitle } from './title'
test('normalizes whitespace in a note title', () => {
expect(normalizeTitle(' Learn Bun ')).toBe('Learn Bun')
})
Run every test in the project:
bun test
Bun prints something like:
title.test.ts:
✓ normalizes whitespace in a note title [0.35ms]
1 pass
0 fail
1 expect() calls
Ran 1 test across 1 file. [15.00ms]
Bun finds test files by name. It looks for .test.ts, _test.ts, .spec.ts, and _spec.ts (and the .js, .tsx, .jsx variants). No config file tells it where to look.
The test has three small parts, and almost every test you’ll write has the same three:
- provide an input
- call the function
- compare the result with the expected value
See a failure
Break the function on purpose. Change ' ' in title.ts to ' ' (two spaces) and run bun test again:
✗ normalizes whitespace in a note title
error: expect(received).toBe(expected)
Expected: "Learn Bun"
Received: "Learn Bun"
Bun shows both values side by side. A good test failure tells you what went wrong before you open the code. Put the single space back.
Add an edge case, a title that’s already clean:
test('keeps a title that is already clean', () => {
expect(normalizeTitle('Build the API')).toBe('Build the API')
})
Useful flags
Run the tests again whenever files change:
bun test --watch
Run only the files whose path matches a string, useful when one area is under work:
bun test title
And generate a coverage report:
bun test --coverage
Coverage tells you which lines executed while the tests ran. It does not tell you whether the assertions describe the right behavior. A test that calls every function and asserts nothing gets 100% coverage and catches zero bugs. Prefer a few meaningful cases, with real inputs and edge cases, over many tests that only repeat the implementation.
Lesson completed