
Bun Runtime
- 59 installs
- 14 repo stars
- Updated March 2, 2026
- oakoss/agent-skills
Helps with ai & agent building tasks during AI-assisted development.
About
bun-runtime is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- bun-runtime
- AI & Agent Building
- AI-coding skill
Bun Runtime by the numbers
- 59 all-time installs (skills.sh)
- +2 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #6,524 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/oakoss/agent-skills --skill bun-runtimeAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 59 |
|---|---|
| repo stars | ★ 14 |
| Last updated | March 2, 2026 |
| Repository | oakoss/agent-skills ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
Bun Runtime
Overview
Bun is an all-in-one JavaScript and TypeScript runtime that includes a fast package manager, bundler, test runner, and Node.js-compatible APIs. It natively executes TypeScript and JSX without a separate compilation step.
When to use: Fast server-side JavaScript, TypeScript-first projects, replacing Node.js for better startup performance, built-in SQLite, password hashing, file I/O, HTTP servers, bundling, and testing without external tooling.
When NOT to use: Projects requiring full Node.js ecosystem compatibility (some native modules unsupported), production environments needing battle-tested stability of Node.js, or browser-only code that does not need a runtime.
Quick Reference
| Pattern | API | Key Points |
|---|---|---|
| HTTP server | Bun.serve({ routes, fetch }) | Route-based, static/dynamic routes, per-method handlers |
| File read | Bun.file(path) | Lazy BunFile (Blob), .text(), .json(), .stream() |
| File write | Bun.write(dest, data) | Accepts string, Blob, Response, BunFile |
| SQLite | new Database(path) from bun:sqlite | Synchronous queries, prepared statements, WAL mode |
| Password hash | Bun.password.hash(pw) | Argon2id default, bcrypt option, async and sync variants |
| Password verify | Bun.password.verify(pw, hash) | Auto-detects algorithm from hash format |
| Bundler | Bun.build({ entrypoints, outdir }) | Tree-shaking, code splitting, plugins, multiple targets |
| Test runner | import { test, expect } from "bun:test" | Jest-compatible, mocking, snapshots, watch mode |
| Install packages | bun install | Fast lockfile resolution, npm-compatible |
| Add package | bun add <pkg> | -d for dev, -g for global |
| Run script | bun run <script> | Runs package.json scripts or files directly |
| Execute binary | bunx <pkg> | Like npx, runs without installing |
| S3 client | new S3Client(opts) / s3.file(key) | Built-in S3-compatible storage client |
| HTML imports | import page from './index.html' | Fullstack: import HTML as route handler |
Common Mistakes
| Mistake | Correct Pattern |
|---|---|
Using fetch handler only without routes | Use routes object for static/dynamic routing (Bun v1.2.3+), fetch as fallback |
Forgetting await on Bun.write() | Bun.write() is async, always await it |
Using Bun.file(path).text() without await | .text(), .json(), .arrayBuffer() all return Promises |
| Creating SQLite database without WAL mode | Enable WAL for concurrent reads: db.exec("PRAGMA journal_mode = WAL") |
Using bun install without --frozen-lockfile in CI | Use bun install --frozen-lockfile for reproducible CI builds |
Importing jest globals in Bun tests | Import from bun:test, not @jest/globals or vitest |
Using node_modules/.bin/ directly | Use bunx or bun run instead of referencing bin paths |
Expecting Bun.build() to throw on failure | Check result.success boolean, errors are in result.logs |
Using --target node when deploying to Bun | Use --target bun for Bun-specific optimizations and bytecode |
| Synchronous password hashing in request handlers | Use await Bun.password.hash() async variant in servers |
Delegation
- Project scaffolding: Use
Exploreagent - Performance profiling: Use
Taskagent - Code review: Delegate to
code-revieweragent
If the typescript-patterns skill is available, delegate advanced TypeScript typing questions to it.References
- Runtime APIs: Bun.serve(), Bun.file(), SQLite, password hashing, and utilities
- Package management: install, add, remove, workspaces, lockfile
- Bundler: Bun.build(), entrypoints, plugins, tree-shaking
- Testing: bun:test, assertions, mocking, snapshots, lifecycle hooks
Bundler
Basic Usage
Programmatic API
const result = await Bun.build({
entrypoints: ['./src/index.ts'],
outdir: './dist',
});
if (!result.success) {
console.error('Build failed:');
for (const log of result.logs) {
console.error(log);
}
process.exit(1);
}
for (const output of result.outputs) {
console.log(`${output.kind}: ${output.path} (${output.size} bytes)`);
}CLI Equivalent
bun build ./src/index.ts --outdir ./distConfiguration Options
Full Configuration
const result = await Bun.build({
entrypoints: ['./src/index.tsx', './src/worker.ts'],
outdir: './dist',
target: 'browser',
format: 'esm',
splitting: true,
minify: true,
sourcemap: 'linked',
external: ['react', 'react-dom'],
define: {
'process.env.API_URL': JSON.stringify('https://api.example.com'),
},
naming: {
entry: '[dir]/[name]-[hash].[ext]',
chunk: 'chunks/[name]-[hash].[ext]',
asset: 'assets/[name]-[hash].[ext]',
},
drop: ['console', 'debugger'],
banner: '"use client";',
footer: '// Built with Bun',
});CLI Flags
bun build ./src/index.tsx --outdir ./dist \
--target browser \
--format esm \
--splitting \
--minify \
--sourcemap=linked \
--external react \
--external react-domTarget
| Target | Description | Output |
|---|---|---|
"browser" | Default. Standard web browsers | Standard ESM/IIFE |
"bun" | Bun runtime | Bun-optimized, supports bun: imports |
"node" | Node.js runtime | Node-compatible, respects node: imports |
await Bun.build({
entrypoints: ['./src/server.ts'],
outdir: './dist',
target: 'bun',
});Format
| Format | Description |
|---|---|
"esm" | Default. ES modules with import/export |
"cjs" | CommonJS with require/module.exports |
"iife" | Immediately Invoked Function Expression for <script> tags |
Code Splitting
await Bun.build({
entrypoints: ['./src/index.ts', './src/admin.ts'],
outdir: './dist',
splitting: true,
});Shared modules between entrypoints are extracted into separate chunks. Only works with "esm" format.
Minification
await Bun.build({
entrypoints: ['./src/index.ts'],
outdir: './dist',
minify: true,
});
await Bun.build({
entrypoints: ['./src/index.ts'],
outdir: './dist',
minify: {
whitespace: true,
identifiers: true,
syntax: true,
},
});Source Maps
| Option | Description |
|---|---|
"none" | No source maps (default) |
"linked" | Separate .map file with reference comment |
"inline" | Embedded in output as data URL |
"external" | Separate .map file without reference comment |
Loaders
Map file extensions to built-in loaders:
await Bun.build({
entrypoints: ['./src/index.tsx'],
outdir: './dist',
loader: {
'.png': 'dataurl',
'.txt': 'file',
'.svg': 'text',
},
});Built-in Loaders
| Loader | Extensions | Description |
|---|---|---|
js | .js, .mjs, .cjs | JavaScript |
jsx | .jsx | JavaScript + JSX |
ts | .ts, .mts, .cts | TypeScript |
tsx | .tsx | TypeScript + JSX |
json | .json | JSON, imported as object |
toml | .toml | TOML, imported as object |
text | .txt | Import as string |
file | any | Copy to outdir, import as path |
dataurl | any | Import as base64 data URL |
External Packages
Exclude packages from the bundle:
await Bun.build({
entrypoints: ['./src/index.ts'],
outdir: './dist',
external: ['express', 'pg', '@prisma/client'],
});Plugins
Plugin Structure
import { type BunPlugin } from 'bun';
const envPlugin: BunPlugin = {
name: 'env-loader',
setup(build) {
build.onResolve({ filter: /^env$/ }, (args) => {
return { path: args.path, namespace: 'env' };
});
build.onLoad({ filter: /.*/, namespace: 'env' }, () => {
return {
contents: `export default ${JSON.stringify(Bun.env)}`,
loader: 'json',
};
});
},
};
await Bun.build({
entrypoints: ['./src/index.ts'],
outdir: './dist',
plugins: [envPlugin],
});Plugin Hooks
| Hook | Description |
|---|---|
onResolve | Intercept module resolution, remap paths |
onLoad | Intercept module loading, transform content |
onResolve Handler
build.onResolve({ filter: /\.yaml$/ }, (args) => {
return {
path: resolve(args.importer, '..', args.path),
namespace: 'yaml',
};
});onLoad Handler
build.onLoad({ filter: /\.yaml$/, namespace: 'yaml' }, async (args) => {
const text = await Bun.file(args.path).text();
const parsed = YAML.parse(text);
return {
contents: `export default ${JSON.stringify(parsed)}`,
loader: 'json',
};
});Define Global Constants
await Bun.build({
entrypoints: ['./src/index.ts'],
outdir: './dist',
define: {
'process.env.NODE_ENV': JSON.stringify('production'),
'process.env.API_URL': JSON.stringify('https://api.example.com'),
__DEV__: 'false',
},
});Bytecode Compilation
Generate bytecode for faster startup (Bun target only):
await Bun.build({
entrypoints: ['./src/index.ts'],
outdir: './dist',
target: 'bun',
bytecode: true,
});bun build ./src/index.ts --outdir ./dist --bytecodeBytecode is limited to cjs format with target: "bun".
Build Result
const result = await Bun.build({
entrypoints: ['./src/index.ts'],
outdir: './dist',
});
result.success; // boolean
result.outputs; // BuildArtifact[]
result.logs; // BuildMessage[]
for (const output of result.outputs) {
output.path; // Absolute file path
output.size; // Size in bytes
output.kind; // "entry-point" | "chunk" | "asset"
output.hash; // Content hash
await output.text(); // Read as string
await output.arrayBuffer(); // Read as ArrayBuffer
}Package Management
Installing Dependencies
Basic Install
bun installReads package.json and writes bun.lockb (binary lockfile). Faster than npm/yarn/pnpm due to native resolution and hardlink-based caching.
CI Mode (Frozen Lockfile)
bun install --frozen-lockfileFails if bun.lockb would change. Use in CI pipelines for reproducible builds.
Install Options
bun install # Install all dependencies
bun install --production # Skip devDependencies
bun install --frozen-lockfile # Fail if lockfile would change
bun install --no-save # Don't update package.json
bun install --dry-run # Show what would be installed
bun install --force # Re-download all packagesAdding and Removing Packages
Add Packages
bun add express # Production dependency
bun add -d typescript # Dev dependency (--dev)
bun add -D @types/node # Dev dependency (alias)
bun add --optional fsevents # Optional dependency
bun add -g create-vite # Global install
bun add zod@3.22 # Specific version
bun add zod@latest # Latest versionRemove Packages
bun remove lodash
bun remove -g create-vite # Remove globalUpdate Packages
bun update # Update all to latest compatible
bun update react # Update specific package
bun update --latest # Ignore semver rangesRunning Scripts
Package.json Scripts
bun run build # Run "build" script
bun run dev # Run "dev" script
bun run test # Run "test" scriptRun Files Directly
bun run index.ts # Run TypeScript directly
bun run script.js # Run JavaScript
bun index.ts # Shorthand (omit "run")Execute Without Installing
bunx create-vite my-app # Like npx
bunx prisma generate # Run package binary
bunx --bun vitest # Force Bun runtime for the packageWorkspaces
Configuration
{
"name": "my-monorepo",
"workspaces": ["packages/*", "apps/*"]
}Workspace Commands
bun install # Install all workspace dependencies
bun add -d typescript --filter "packages/*" # Add to filtered workspacesCross-Workspace Dependencies
{
"name": "@myorg/web",
"dependencies": {
"@myorg/shared": "workspace:*"
}
}The workspace:* protocol links to the local workspace package. Bun resolves these as symlinks.
Lockfile
Bun uses bun.lockb, a binary lockfile optimized for speed.
Inspecting the Lockfile
bun bun.lockb # Print human-readable lockfileGenerating a Yarn-Compatible Lockfile
bun install --yarn # Also generate yarn.lockLockfile in Version Control
Always commit bun.lockb to version control. It ensures deterministic installs across environments.
Overrides and Resolutions
Overrides (package.json)
{
"overrides": {
"lodash": "4.17.21",
"react": "$react"
}
}- Force a specific version for all nested dependencies
- Use
$packageNameto reference the version in your own dependencies
Trusted Dependencies
{
"trustedDependencies": ["@prisma/client", "esbuild"]
}Only packages listed in trustedDependencies can run postinstall scripts. This is a security feature enabled by default in Bun.
Patching Packages
bun patch express # Extract package for editing
# Make changes to node_modules/express/...
bun patch --commit express # Save patchPatches are stored in patches/ directory and applied automatically on install.
Global Configuration
bunfig.toml
[install]
# Registry configuration
registry = "https://registry.npmjs.org"
# Scoped registries
[install.scopes]
"@myorg" = "https://npm.myorg.com"
# Cache directory
[install.cache]
dir = "~/.bun/install/cache"Environment Variables
BUN_INSTALL_CACHE_DIR=~/.bun/cache # Custom cache location
BUN_CONFIG_REGISTRY=https://registry.npmjs.org # Default registryMigration from npm/yarn/pnpm
Bun reads package.json and is compatible with the npm registry. To migrate:
rm -rf node_modules package-lock.json yarn.lock pnpm-lock.yaml
bun installBun generates bun.lockb. The node_modules structure is flat (similar to npm), using hardlinks from the global cache for disk efficiency.
Compatibility Notes
package.jsonscripts work as-isnode_moduleslayout is compatible with Node.js tooling- Lifecycle scripts (
postinstall,prepare) run only fortrustedDependencies .npmrcis partially supported (registry and auth token settings)
Runtime APIs
HTTP Server with Bun.serve()
Basic Server with Routes
const server = Bun.serve({
port: 3000,
routes: {
'/api/health': new Response('OK'),
'/api/users/:id': (req) => {
return Response.json({ id: req.params.id });
},
'/api/users': {
GET: () => Response.json([]),
POST: async (req) => {
const body = await req.json();
return Response.json(body, { status: 201 });
},
},
'/favicon.ico': Bun.file('./public/favicon.ico'),
'/api/*': Response.json({ error: 'Not found' }, { status: 404 }),
},
fetch(req) {
return new Response('Not Found', { status: 404 });
},
error(error) {
console.error(error);
return new Response('Internal Server Error', { status: 500 });
},
});
console.log(`Server running at ${server.url}`);Route Features
- Static responses: Map path to
new Response()orResponse.json() - Dynamic routes: Use
:paramsyntax, access viareq.params.id - Per-method handlers: Object with
GET,POST,PUT,DELETEkeys - Wildcard routes: Use
*for catch-all matching - File serving: Map path to
Bun.file()for static assets - Redirects:
Response.redirect("/new-path")
Hot Reload and Shutdown
server.reload({
routes: {
'/api/version': () => Response.json({ version: '2.0.0' }),
},
});
await server.stop();WebSocket Support
Bun.serve({
fetch(req, server) {
if (server.upgrade(req)) return;
return new Response('Not a WebSocket request', { status: 400 });
},
websocket: {
open(ws) {
ws.subscribe('chat');
},
message(ws, message) {
ws.publish('chat', message);
},
close(ws) {
ws.unsubscribe('chat');
},
},
});File I/O
Reading Files with Bun.file()
const file = Bun.file('./config.json');
console.log(`Size: ${file.size} bytes`);
console.log(`Type: ${file.type}`);
const exists = await file.exists();
const text = await file.text();
const json = await file.json();
const buffer = await file.arrayBuffer();
const bytes = await file.bytes();
const stream = file.stream();Writing Files with Bun.write()
await Bun.write('output.txt', 'Hello, World!');
await Bun.write('data.json', JSON.stringify({ key: 'value' }));
await Bun.write('copy.txt', Bun.file('original.txt'));
const response = await fetch('https://example.com');
await Bun.write('page.html', response);Incremental Writing with FileSink
const writer = Bun.file('log.txt').writer({ highWaterMark: 1024 * 1024 });
writer.write('Line 1\n');
writer.write('Line 2\n');
writer.flush();
writer.end();Delete a File
await Bun.file('temp.txt').delete();Write to stdout
await Bun.write(Bun.stdout, 'Output to terminal\n');SQLite with bun:sqlite
Basic Usage
import { Database } from 'bun:sqlite';
const db = new Database('app.db');
db.exec('PRAGMA journal_mode = WAL');
db.exec(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL
)
`);Queries and Prepared Statements
const insert = db.prepare('INSERT INTO users (name, email) VALUES (?, ?)');
insert.run('Alice', 'alice@example.com');
const user = db.query('SELECT * FROM users WHERE id = ?').get(1);
const allUsers = db.query('SELECT * FROM users').all();
const names = db.query('SELECT name FROM users').values();Transactions
const insertMany = db.transaction(
(users: { name: string; email: string }[]) => {
const insert = db.prepare(
'INSERT INTO users (name, email) VALUES ($name, $email)',
);
for (const user of users) {
insert.run(user);
}
},
);
insertMany([
{ name: 'Bob', email: 'bob@example.com' },
{ name: 'Carol', email: 'carol@example.com' },
]);In-Memory Database
const memDb = new Database(':memory:');Password Hashing
Argon2id (Default)
const hash = await Bun.password.hash('super-secure-password');
const isValid = await Bun.password.verify('super-secure-password', hash);Bcrypt
const bcryptHash = await Bun.password.hash('password', {
algorithm: 'bcrypt',
cost: 10,
});
const isValid = await Bun.password.verify('password', bcryptHash);Synchronous Variants
const hash = Bun.password.hashSync('password');
const isValid = Bun.password.verifySync('password', hash);S3 Client
Bun includes a built-in S3 client compatible with any S3-compatible storage (AWS, R2, MinIO).
Basic S3 Operations
import { S3Client } from 'bun';
const s3 = new S3Client({
accessKeyId: Bun.env.AWS_ACCESS_KEY_ID,
secretAccessKey: Bun.env.AWS_SECRET_ACCESS_KEY,
region: 'us-east-1',
bucket: 'my-bucket',
});
const file = s3.file('uploads/photo.jpg');
const exists = await file.exists();
const content = await file.text();
await s3.file('output.json').write(JSON.stringify({ key: 'value' }));List Objects
import { s3 } from 'bun';
const objects = await s3.list({ prefix: 'uploads/' });
for (const obj of objects) {
console.log(obj.key, obj.size);
}Presigned URLs
const url = s3.presign('uploads/photo.jpg', {
expiresIn: 3600,
method: 'GET',
});HTML Imports (Fullstack)
Import HTML files directly as route handlers. Bun automatically bundles associated scripts and styles.
import homepage from './index.html';
import dashboard from './dashboard.html';
Bun.serve({
routes: {
'/': homepage,
'/dashboard': dashboard,
},
fetch(req) {
return new Response('Not Found', { status: 404 });
},
});Production Build
await Bun.build({
entrypoints: ['./index.html'],
outdir: './dist',
minify: true,
});Utility Functions
Bun.sleep()
await Bun.sleep(1000);
await Bun.sleep('5s');Bun.hash()
const hash = Bun.hash('hello world');
const wyhash = Bun.hash.wyhash('data');
const adler32 = Bun.hash.adler32('data');
const crc32 = Bun.hash.crc32('data');Bun.peek()
const value = Bun.peek(promise);Bun.which()
const path = Bun.which('node');Environment Variables
const apiKey = Bun.env.API_KEY;
const port = Bun.env.PORT ?? '3000';Bun.version
console.log(Bun.version);
console.log(Bun.revision);Server + SQLite Integration
import { Database } from 'bun:sqlite';
const db = new Database('posts.db');
db.exec('PRAGMA journal_mode = WAL');
db.exec(`
CREATE TABLE IF NOT EXISTS posts (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
content TEXT NOT NULL,
created_at TEXT NOT NULL
)
`);
Bun.serve({
routes: {
'/api/posts': {
GET: () => {
const posts = db.query('SELECT * FROM posts').all();
return Response.json(posts);
},
POST: async (req) => {
const { title, content } = await req.json();
const id = crypto.randomUUID();
db.query(
'INSERT INTO posts (id, title, content, created_at) VALUES (?, ?, ?, ?)',
).run(id, title, content, new Date().toISOString());
return Response.json({ id, title, content }, { status: 201 });
},
},
'/api/posts/:id': (req) => {
const post = db
.query('SELECT * FROM posts WHERE id = ?')
.get(req.params.id);
if (!post) return new Response('Not Found', { status: 404 });
return Response.json(post);
},
},
fetch(req) {
return new Response('Not Found', { status: 404 });
},
});Testing
Basic Tests
import { test, expect } from 'bun:test';
test('arithmetic', () => {
expect(2 + 2).toBe(4);
});
test('async fetch', async () => {
const response = await fetch('https://api.example.com/health');
expect(response.ok).toBe(true);
});Describe Blocks
import { describe, test, expect } from 'bun:test';
describe('Math operations', () => {
test('addition', () => {
expect(1 + 1).toBe(2);
});
test('multiplication', () => {
expect(2 * 3).toBe(6);
});
describe('nested: division', () => {
test('basic division', () => {
expect(10 / 2).toBe(5);
});
});
});Lifecycle Hooks
import {
describe,
test,
expect,
beforeAll,
afterAll,
beforeEach,
afterEach,
} from 'bun:test';
import { Database } from 'bun:sqlite';
describe('database tests', () => {
let db: Database;
beforeAll(() => {
db = new Database(':memory:');
db.exec('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)');
});
afterAll(() => {
db.close();
});
beforeEach(() => {
db.exec("INSERT INTO users (name) VALUES ('Alice')");
});
afterEach(() => {
db.exec('DELETE FROM users');
});
test('user exists', () => {
const user = db.query('SELECT * FROM users WHERE name = ?').get('Alice');
expect(user).toBeDefined();
});
});Assertions
Common Matchers
import { test, expect } from 'bun:test';
test('matchers', () => {
expect(42).toBe(42);
expect({ a: 1 }).toEqual({ a: 1 });
expect(10).toBeGreaterThan(5);
expect(3).toBeLessThanOrEqual(3);
expect('hello world').toContain('world');
expect('hello').toMatch(/^hel/);
expect(null).toBeNull();
expect(undefined).toBeUndefined();
expect(1).toBeDefined();
expect(true).toBeTruthy();
expect(0).toBeFalsy();
expect([1, 2, 3]).toHaveLength(3);
expect({ name: 'Alice' }).toHaveProperty('name');
expect({ name: 'Alice' }).toHaveProperty('name', 'Alice');
});Error Assertions
import { test, expect } from 'bun:test';
test('throws', () => {
expect(() => {
throw new Error('fail');
}).toThrow('fail');
expect(() => {
throw new TypeError('type error');
}).toThrow(TypeError);
});Async Assertions
import { test, expect } from 'bun:test';
test('resolves', async () => {
await expect(Promise.resolve(42)).resolves.toBe(42);
});
test('rejects', async () => {
await expect(Promise.reject(new Error('oops'))).rejects.toThrow('oops');
});Negation
import { test, expect } from 'bun:test';
test('negation', () => {
expect(1).not.toBe(2);
expect('hello').not.toContain('xyz');
expect(null).not.toBeDefined();
});Mocking
Mock Functions
import { test, expect, mock } from 'bun:test';
test('mock function', () => {
const fn = mock((x: number) => x * 2);
fn(5);
fn(10);
expect(fn).toHaveBeenCalledTimes(2);
expect(fn).toHaveBeenCalledWith(5);
expect(fn.mock.calls).toEqual([[5], [10]]);
expect(fn.mock.results[0].value).toBe(10);
});Mock Return Values
import { test, expect, mock } from 'bun:test';
test('mock return values', () => {
const fn = mock(() => 'default');
fn.mockReturnValueOnce('first');
fn.mockReturnValueOnce('second');
expect(fn()).toBe('first');
expect(fn()).toBe('second');
expect(fn()).toBe('default');
});spyOn
import { test, expect, spyOn } from 'bun:test';
test('spy on method', () => {
const obj = {
method(x: number) {
return x + 1;
},
};
const spy = spyOn(obj, 'method');
obj.method(5);
expect(spy).toHaveBeenCalled();
expect(spy).toHaveBeenCalledWith(5);
spy.mockReturnValue(100);
expect(obj.method(5)).toBe(100);
spy.mockRestore();
expect(obj.method(5)).toBe(6);
});Module Mocking
import { test, expect, mock } from 'bun:test';
mock.module('./utils', () => ({
calculate: () => 42,
}));
test('mocked module', async () => {
const { calculate } = await import('./utils');
expect(calculate()).toBe(42);
});Snapshots
import { test, expect } from 'bun:test';
test('snapshot', () => {
const user = { name: 'Alice', role: 'admin' };
expect(user).toMatchSnapshot();
});
test('inline snapshot', () => {
const value = { x: 1, y: 2 };
expect(value).toMatchInlineSnapshot(`
{
"x": 1,
"y": 2,
}
`);
});Update snapshots:
bun test --update-snapshotsTest Modifiers
import { test, describe } from 'bun:test';
test.skip('not implemented yet', () => {});
test.todo('implement later');
test.only('run only this test', () => {});
describe.skip('skip entire suite', () => {
test('skipped', () => {});
});Concurrent Tests
import { test, expect } from 'bun:test';
test.concurrent('parallel 1', async () => {
await Bun.sleep(100);
expect(true).toBe(true);
});
test.concurrent('parallel 2', async () => {
await Bun.sleep(100);
expect(true).toBe(true);
});Timeouts
import { test, expect } from 'bun:test';
test('slow operation', async () => {
const result = await slowOperation();
expect(result).toBeDefined();
}, 10_000);CLI Commands
bun test # Run all tests
bun test --watch # Watch mode
bun test --coverage # Show code coverage
bun test -t "pattern" # Filter by test name
bun test src/utils # Run tests in directory
bun test --timeout 30000 # Set global timeout (ms)
bun test --bail # Stop after first failure
bun test --bail 5 # Stop after 5 failures
bun test --rerun-each 3 # Run each test 3 timesFile Conventions
Bun auto-discovers test files matching these patterns:
*.test.ts,*.test.tsx,*.test.js,*.test.jsx*_test.ts,*_test.tsx,*_test.js,*_test.jsx*.spec.ts,*.spec.tsx,*.spec.js,*.spec.jsx
Coverage
bun test --coverageOutput shows line-by-line coverage per file. Configure thresholds in bunfig.toml:
[test]
coverage = true
coverageThreshold = { line = 80, function = 80, statement = 80 }
coverageReporter = ["text", "lcov"]Testing HTTP Servers
import { test, expect, afterAll } from 'bun:test';
const server = Bun.serve({
port: 0,
fetch(req) {
return Response.json({ status: 'ok' });
},
});
afterAll(() => {
server.stop();
});
test('health check', async () => {
const response = await fetch(`${server.url}api/health`);
expect(response.status).toBe(404);
const root = await fetch(server.url);
const body = await root.json();
expect(body).toEqual({ status: 'ok' });
});