bun:test vs node:test vs Vitest 2026
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) |
Ecosystem & Community
Vitest has become the dominant JavaScript testing framework for new projects in 2026, surpassing Jest's historical position as the default. With 7M+ weekly downloads and deep integration with the Vite ecosystem, it's the first recommendation in virtually every modern framework's documentation. The ecosystem around Vitest is broad: @testing-library integration works out of the box, @vitest/browser provides Playwright-backed browser testing, and the Storybook integration runs component tests in isolation. For a broader comparison of testing tools including E2E frameworks, see best JavaScript testing frameworks 2026.
bun:test's community is growing alongside Bun itself. The Bun team treats the test runner as a first-class feature, and compatibility improvements arrive with every Bun release. The community actively reports Jest compatibility issues, which the Bun team addresses quickly. For teams already invested in Bun's ecosystem, bun:test is a natural fit.
node:test is intentionally minimal — the Node.js team's philosophy is that the built-in runner should be good enough for simple cases without bloating Node.js itself. The TAP output format integration means it works with established CI tooling, and the fact that it requires no npm packages makes it genuinely useful for testing utility scripts and CLI tools in environments where npm dependencies are undesirable.
Real-World Adoption
Vitest is the test runner of choice for new Next.js, Nuxt, Astro, and SvelteKit projects in 2026. The official documentation for each of these frameworks recommends Vitest. Linear, Vercel, and many modern SaaS companies have migrated their JavaScript test suites from Jest to Vitest, citing faster watch mode and better TypeScript integration as primary motivations.
bun:test sees heavy adoption in projects that have fully committed to Bun as their runtime. Companies building backend services or CLI tools in Bun use bun:test for its zero-dependency approach and performance. The migration story from Jest is easy enough that some Node.js projects have switched to bun:test purely for the speed improvement, while keeping Node.js as their production runtime.
node:test is increasingly used for testing Node.js built-in modules and for projects where the test suite complexity is low. Several popular Node.js packages have switched from Jest to node:test to reduce their devDependency footprint, accepting the trade-off of less expressive assertions.
Developer Experience Deep Dive
Vitest has the best developer experience of the three. The watch mode with HMR means test re-runs start in milliseconds after a file change, not after a full restart. The UI mode provides a visual test explorer that's invaluable for large test suites. TypeScript support is native — no compilation step required. The module mocking API (vi.mock, vi.spyOn) is comprehensive and well-documented. Error messages include source maps that point to the exact line in TypeScript source.
bun:test's developer experience is close to Vitest for the basics. TypeScript support is native because Bun transpiles TypeScript directly. Watch mode works well. The main gaps are UI mode (not available), inline snapshots (not supported), and some Jest features that aren't yet implemented. For developers who primarily work in their editor and rarely need a visual test runner, bun:test's experience is essentially equivalent to Vitest.
node:test's developer experience is deliberately spartan. The assert module is powerful but verbose compared to expect. There's no watch mode with hot reloading. TypeScript requires an external loader (tsx or ts-node). Coverage is experimental. These limitations are acceptable for simple scripts but become significant friction in complex test suites.
Final Verdict 2026
Vitest is the recommended default for new JavaScript and TypeScript projects in 2026. The 5-30 second setup cost (installing Vitest and optionally configuring it) is worthwhile for the ecosystem benefits: comprehensive coverage reporting, UI mode, browser testing, and the best TypeScript integration available. The Jest compatibility means existing test code migrates with minimal changes.
bun:test is the right choice for projects already using Bun as their runtime. The zero-setup story is genuinely compelling, and the performance advantage is real for CPU-bound unit test suites. If your project is Bun-native and you don't need UI mode or inline snapshots, bun:test provides a better default-on experience.
node:test occupies a specific niche: projects that want test coverage without npm dependencies. CLI tools, utility scripts, and simple modules are ideal candidates. Don't choose node:test for a React application or any project where mocking, snapshots, or comprehensive assertions are needed.
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
For integration testing that pairs with these runners, see Testcontainers Node.js vs Docker Compose 2026. For the broader JavaScript testing framework landscape, see best JavaScript testing frameworks 2026.
Choosing a Runner for 2026
The test runner landscape has stabilized around Vitest for most new projects. The combination of Vite's ecosystem integration, Jest-compatible API, and actively maintained browser mode gives it the broadest applicability. If you're already using Vite, Vitest is the natural default.
bun:test makes sense if you're building a Bun-native project or if raw test execution speed is a top priority and you're not relying on Jest-specific plugins. The speed advantage is real on large test suites with many fast unit tests.
node:test is worth knowing about for projects that must minimize dependencies — CLI tools, libraries intended for distribution, or environments where installing test framework dependencies is undesirable. It's also the right foundation for understanding what the runtime provides before adding a framework layer.
The switching cost between these runners is lower than it used to be. Vitest and bun:test both target Jest API compatibility, so migrating test files is mostly mechanical. The bigger switching cost is usually mocking configuration and snapshot format differences. Plan for a day of cleanup, not a week of rewrites.