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:

  1. provide an input
  2. call the function
  3. 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