bun:test vs node:test vs Vitest JavaScript Testing 2026
bun:test vs node:test vs Vitest: The 2026 State of Zero-Config JavaScript Testing
TL;DR
The testing landscape has fractured into two camps in 2026: runtime-native testing (built into Bun and Node.js, zero dependencies, just run it) vs dedicated test frameworks (Vitest, Jest — richer features, broader ecosystem, some config required). bun:test is the fastest option by a wide margin — the same JavaScript runtime that runs your code also runs the tests, with Jest-compatible APIs. node:test is the Node.js answer — stable since Node.js 18, no npm install needed, but significantly fewer features. Vitest remains the recommended choice for most projects: HMR-based watch mode, UI mode, Storybook integration, and the deepest ecosystem — it just requires Vite as a build tool. The right answer depends on whether you prioritize zero dependencies or rich testing capabilities.
Key Takeaways
- bun:test is 5-20x faster than Jest/Vitest in benchmarks — Bun's JS engine speed + native test runner; compatible with most Jest APIs so migration is often zero-code-change
- node:test is the zero-install option for Node.js projects — ships with Node.js 18+, now stable; uses
node:assertfor assertions, lacks many modern features - Vitest is the most feature-rich — watch mode, UI mode, browser mode, TypeScript-first, best ecosystem; requires Vite (5-30 second initial setup)
- Performance reality: for most projects, test suite time is dominated by I/O (database queries, API calls, file system) not test runner overhead — bun:test's speed matters most for large suites of fast unit tests
- Jest compatibility: bun:test is ~95% compatible; node:test requires rewriting to the
test()+assertAPI; Vitest is ~99% compatible - Recommendation: use Vitest for new projects; consider bun:test if you're already on Bun; use node:test for simple scripts and CLI utilities that shouldn't have npm dependencies
The Three Testing Approaches
Runtime-Native (bun:test, node:test)
The philosophy: your runtime should include a test runner. Zero installation, no config files for simple cases, just write and run:
# Bun
bun test # Runs all *.test.ts files
bun test --watch # Watch mode
# Node.js
node --test # Runs all *.test.js files
node --test src/**/*.test.js # Specific files
Dedicated Framework (Vitest, Jest)
The philosophy: testing is complex enough to deserve a specialized tool with a rich ecosystem:
# Install once
npm install -D vitest
# Configure (optional, Vite project = auto-configured)
# Run
npx vitest # Watch mode
npx vitest run # Single run
npx vitest --ui # Browser-based UI
bun:test: The Speed Champion
bun:test is built into Bun and uses Jest's API — describe, test, expect, beforeEach, afterEach, mock.
Setup: Literally None
# Write a test file
cat > calculator.test.ts << 'EOF'
import { test, expect, describe } from 'bun:test'
import { add, subtract } from './calculator'
describe('Calculator', () => {
test('adds numbers', () => {
expect(add(2, 3)).toBe(5)
})
test('subtracts numbers', () => {
expect(subtract(10, 4)).toBe(6)
})
})
EOF
# Run it — no config, no install
bun test
Jest-Compatible API
bun:test is designed as a Jest drop-in replacement for most tests:
// This runs unchanged on Jest, Vitest, AND bun:test
import { describe, test, expect, beforeEach, afterEach, mock } from 'bun:test'
// or just: global describe/test/expect are available without import
describe('UserService', () => {
const mockEmailService = mock(() => Promise.resolve())
beforeEach(() => {
mockEmailService.mockClear()
})
test('sends welcome email on registration', async () => {
const userService = new UserService({ email: mockEmailService })
await userService.register({ email: 'user@example.com', name: 'Alice' })
expect(mockEmailService).toHaveBeenCalledOnce()
expect(mockEmailService).toHaveBeenCalledWith({
to: 'user@example.com',
template: 'welcome',
})
})
test('throws on duplicate email', async () => {
await expect(
userService.register({ email: 'existing@example.com', name: 'Bob' })
).rejects.toThrow('Email already registered')
})
})
Performance: The Main Selling Point
In benchmarks across various test suites:
| Suite Size | Jest | Vitest | bun:test |
|---|---|---|---|
| 100 tests (unit) | 3.2s | 1.8s | 0.4s |
| 500 tests (unit) | 12.1s | 6.3s | 1.2s |
| 1000 tests (unit) | 31s | 15s | 3.1s |
| 1000 tests (with DB) | 45s | 28s | 22s |
The DB-heavy scenario shows why "5-20x faster" is nuanced — when tests are I/O-bound rather than CPU-bound, the runtime speedup matters less.
bun:test Gotchas
Not everything is Jest-compatible:
// ✅ Works in bun:test
jest.fn() // Use mock() instead
jest.mock('./module') // Use mock.module() instead
jest.spyOn(object, 'method') // Available
// ⚠️ Different API
import { mock } from 'bun:test'
mock.module('./analytics', () => ({
track: mock(() => {}),
}))
// ❌ Not available in bun:test
jest.useFakeTimers() // Use Bun's --fake-timers flag
jest.runAllTimers() // CLI-only, not per-test
// ✅ Snapshot testing works
expect(component).toMatchSnapshot()
node:test: Built Into Node.js
node:test became stable in Node.js 18 and is now the official Node.js test runner. No npm install, no configuration:
import { test, describe, before, after, mock } from 'node:test'
import assert from 'node:assert/strict'
describe('UserService', () => {
let userService: UserService
before(() => {
userService = new UserService({ db: testDb })
})
after(async () => {
await testDb.cleanup()
})
test('creates a user successfully', async () => {
const user = await userService.create({
name: 'Alice',
email: 'alice@example.com',
})
assert.equal(user.name, 'Alice')
assert.equal(user.email, 'alice@example.com')
assert.ok(user.id, 'User should have an ID')
})
test('rejects invalid email', async () => {
await assert.rejects(
() => userService.create({ name: 'Bob', email: 'not-an-email' }),
{ message: /invalid email/i }
)
})
})
Running node:test
# Single file
node --test user.test.ts
# All test files recursively
node --test '**/*.test.{js,ts}'
# Watch mode (Node.js 22+)
node --test --watch
# Coverage (built-in since Node.js 22)
node --test --experimental-test-coverage
node:test Limitations
node:test is deliberately minimal:
// ❌ No expect().toBe() — use assert
// ❌ No watch mode with HMR
// ❌ No UI mode
// ❌ No TypeScript support without ts-node/tsx
// ❌ No snapshot testing
// ❌ No module mocking (use --import hooks manually)
// ❌ No concurrent test execution across files by default
// ❌ No coverage UI — just JSON/TAP output
// ✅ What it does well:
// → Zero dependencies
// → TAP output (works with any TAP reporter)
// → Subtest support
// → Built-in diagnostics
For CLI utilities, scripts, and simple modules that you don't want to add testing infrastructure to, node:test is ideal. For complex applications, its limitations become friction.
Vitest: The Feature-Rich Default
Vitest (7M+ weekly downloads) requires Vite but provides the most complete testing experience:
import { describe, test, expect, vi, beforeEach } from 'vitest'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
// TypeScript support, module mocking, happy-dom/JSDOM, browser mode, UI mode
vi.mock('./analytics', () => ({
track: vi.fn(),
}))
describe('SearchInput', () => {
test('calls onChange debounced', async () => {
vi.useFakeTimers()
const onChange = vi.fn()
render(<SearchInput onChange={onChange} debounceMs={300} />)
const input = screen.getByRole('textbox')
await userEvent.type(input, 'hello')
// No calls yet — debounced
expect(onChange).not.toHaveBeenCalled()
// Advance timers
vi.advanceTimersByTime(300)
expect(onChange).toHaveBeenCalledOnce()
expect(onChange).toHaveBeenCalledWith('hello')
vi.useRealTimers()
})
})
Vitest's Unique Features
UI Mode — browser-based test runner UI:
npx vitest --ui
# Opens browser UI with:
# - Test results in real-time
# - Source code side-by-side
# - Coverage report
# - Snapshot inspector
Workspace support — run different environments in parallel:
// vitest.config.ts
export default defineConfig({
test: {
workspace: [
{ test: { name: 'unit', environment: 'jsdom' } },
{ test: { name: 'browser', browser: { enabled: true, name: 'chromium' } } },
{ test: { name: 'node', environment: 'node' } },
],
},
})
Migration Paths
From Jest to bun:test (near-zero for most)
# Try it first
bun test # May just work if your tests don't use Jest-specific features
# Common fixes:
# jest.fn() → mock()
# jest.mock() → mock.module()
# jest.useFakeTimers() → run with --fake-timers flag
From Jest to Vitest (~30 min)
npm install -D vitest
# Replace jest.config.js → vitest.config.ts
# Replace import { jest } with import { vi }
# vi.fn(), vi.mock(), vi.spyOn() are all identical
From Jest/Vitest to node:test (significant rewrite)
node:test uses assert instead of expect — the entire assertion style is different. Only worth it if eliminating npm dependencies is a hard requirement.
Coverage and CI Integration
Code coverage is a key differentiator between the three options.
bun:test Coverage
# Built-in coverage (uses V8 coverage)
bun test --coverage
# Output:
# ------------------|---------|----------|---------|---------|
# File | % Stmts | % Branch | % Funcs | % Lines |
# ------------------|---------|----------|---------|---------|
# src/calculator.ts | 95.2% | 87.5% | 100% | 95.2% |
# src/utils.ts | 88.0% | 75.0% | 90% | 88.0% |
# Coverage thresholds (bun 1.1+)
bun test --coverage --coverage-threshold 80
bun:test coverage uses V8's native coverage — fast and accurate. HTML reports aren't built-in but output can be consumed by tools like c8 for HTML generation.
node:test Coverage
# Experimental coverage (Node.js 22+)
node --test --experimental-test-coverage
# With LCOV output for external tools
node --test --experimental-test-coverage --test-reporter=lcov > coverage.lcov
node:test coverage is still experimental — functional but not production-ready for detailed reports.
Vitest Coverage
# Install the V8 provider (fastest)
npm install -D @vitest/coverage-v8
# Run with coverage
npx vitest run --coverage
# Configure thresholds
// vitest.config.ts
export default defineConfig({
test: {
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html', 'lcov'],
thresholds: {
global: {
statements: 80,
branches: 75,
functions: 80,
lines: 80,
},
},
exclude: ['**/node_modules/**', '**/test/**'],
},
},
})
Vitest produces the most comprehensive coverage reports — HTML with line-by-line highlighting, LCOV for CI/CD integration, JSON for custom tooling.
CI/CD Integration
All three work in CI with minimal setup:
# GitHub Actions — bun:test
- name: Run tests
run: bun test --coverage
# GitHub Actions — node:test
- name: Run tests
run: node --test
# GitHub Actions — Vitest
- name: Run tests
run: npx vitest run --reporter=github-actions --coverage
# The github-actions reporter produces inline PR annotations
Vitest's --reporter=github-actions is a notable feature — it posts test results as inline annotations on PRs, making failures immediately visible in code review.
Snapshot Testing
Snapshot testing support varies significantly:
// Vitest — full snapshot support
test('renders correctly', () => {
const { container } = render(<Button>Click me</Button>)
expect(container).toMatchSnapshot()
expect(container).toMatchInlineSnapshot(`
<div>
<button class="btn btn-primary">Click me</button>
</div>
`)
})
// bun:test — snapshot support added in Bun 1.0
test('renders correctly', () => {
expect(renderToString(<Button>Click me</Button>)).toMatchSnapshot()
// Inline snapshots not yet supported
})
// node:test — no snapshot support
// Must use external libraries or implement manually
For snapshot-heavy test suites (React component rendering, API response shapes), Vitest or bun:test are required.
Decision Guide
| Situation | Recommendation |
|---|---|
| New Node.js project with Vite | Vitest |
| New Bun project | bun:test |
| CLI utility (no npm deps) | node:test |
| Migrating from Jest | Vitest (easiest) or bun:test |
| Need browser testing | Vitest browser mode |
| Need UI mode / coverage UI | Vitest |
| Max performance (pure unit tests) | bun:test |
| Enterprise, must stay on Node.js | Vitest or node:test |
| Monorepo with mixed frameworks | Vitest (workspace support) |
Methodology
- Performance benchmarks from community benchmarks (github.com/nicolo-ribaudo/bun-tests-benchmark), March 2026
- Download data from npmjs.com API weekly averages
- Versions: Bun 1.2.x, Node.js 22.x (LTS), Vitest 2.x
- Sources: Bun documentation (bun.sh/docs/cli/test), Node.js documentation (nodejs.org/api/test.html), Vitest documentation (vitest.dev)
Compare Vitest, bun:test, and node:test on PkgPulse — download trends, ecosystem health, and version activity.
Related: node:test vs Vitest vs Jest Native Test Runner 2026 · Vitest Browser Mode vs Playwright Component Testing 2026 · Playwright vs Cypress vs Puppeteer E2E Testing 2026