Vitest vs Bun Test — Speed vs Ecosystem
Bun's test runner boots 11× faster than Vitest. But if your suite uses __mocks__ directories, Istanbul coverage, or @vitest/ui, you'll lose more than you gain. Here's when to switch and when to stay.
By Ethan
1,050 words · 6 min read
Use Bun test for greenfield TypeScript projects with straightforward mocking. Stay on Vitest if your suite has __mocks__ directories, needs Istanbul coverage, or relies on the @vitest/ui dashboard. The cold-start gap is real — 11× — but it disappears fast once you hit Bun’s compatibility ceiling.
Who this is for
Mid-level developers evaluating whether to migrate an existing Vitest suite to Bun, or choosing a test runner for a new TypeScript project. If you’re on Jest, this also applies — but migration paths from Jest to Bun and from Vitest to Bun are different in a few places.
What we tested
Bun 1.3.14 (released 2026-05-13) · Vitest latest (requires Vite ≥6.0.0, Node ≥20.0.0). Benchmark: 50 tests across 10 TypeScript files with mocking, via PkgPulse 2026. The PkgPulse suite is a standard micro-benchmark — not a production test graph. Real-world timings depend on your machine class, transform cost, DOM setup, and database containers.
Cold start: Bun wins, clearly
| Runner | Cold start (50 tests, 10 files, TS + mocking) |
|---|---|
| Bun test | 0.08 s |
| Vitest | 0.9 s |
| Jest | 1.2 s |
Bun is ~11× faster than Vitest on cold start. That number is from a micro-benchmark, but the direction holds across PkgPulse’s more complex suites too.
Watch mode: closer than you’d think
| Runner | Watch re-run | Method |
|---|---|---|
| Vitest | ~40 ms | HMR: re-runs only affected tests |
| Bun test | ~50 ms | Re-runs the matched set |
Ten milliseconds sounds negligible, and for small suites it is. The difference grows in large monorepos where Vitest’s HMR intelligence — re-running only tests transitively affected by the changed file — compounds. Bun runs everything that matched the filter again, every time.
Mocking: Bun’s biggest gap
Both runners cover the standard mocking surface: jest.fn(), spyOn(), module mocking, snapshot testing, and fake timers.
What Bun does not support:
__mocks__directory auto-mocking — Bun’s docs are explicit: not supported yet (Bun mocking docs). Teams that migrated from Jest using the__mocks__pattern cannot switch to Bun without refactoring.vi.mock()hoisting — Bun requires a--preloadworkaround for manual module mocking at the top of test files. Different mental model than Vitest’s transparent hoisting.- Per-file mock isolation — by default,
mock.module()overrides bleed across test files in the same test run.
If your suite uses __mocks__, treat this as a hard blocker. If it doesn’t, mocking is a migration cost, not a wall.
Coverage: Vitest is the deeper option
| Feature | Vitest | Bun test |
|---|---|---|
| V8-based coverage | ✅ | Built-in (custom, not V8 inspector) |
| Istanbul-based coverage | ✅ | ❌ |
| HTML report | ✅ | ❌ |
| LCOV output | ✅ | ✅ |
| UI-integrated coverage | ✅ (@vitest/ui) | ❌ |
| Threshold enforcement | ✅ | ✅ (bunfig.toml) |
Bun coverage does the job for CI gating — --coverage gives you text + LCOV, which feeds Codecov and Coveralls without issue. But if your pipeline uses Istanbul for Cloudflare Workers tests (Bun’s coverage provider doesn’t run in CF’s V8 isolate), you’re locked to Vitest + @vitest/coverage-istanbul. HTML reports are also absent — no in-browser coverage view without an external LCOV-to-HTML tool.
DX tooling: @vitest/ui has no Bun equivalent
@vitest/ui is a browser-based dashboard — interactive test tree, dependency graph, inline coverage, pass/fail filtering. Install with npm i -D @vitest/ui, run with vitest --ui.
Bun’s test reporter formats are dots and junit; coverage output formats are text (default) and lcov. Nothing interactive. If your team has built a workflow around the UI dashboard, there’s no drop-in replacement on the Bun side.
One area where Bun is better out of the box: GitHub Actions annotations. Bun auto-detects the GHA environment and emits inline test failure annotations — zero config. Vitest needs a separate reporter plugin.
CI/CD setup
Bun (GitHub Actions):
- uses: oven-sh/setup-bun@v2
- run: bun install
- run: bun test
Vitest (GitHub Actions):
- uses: actions/setup-node@v4
with: { node-version: '20' }
- run: npm ci
- run: npx vitest run
Three lines vs four. Bun also ships official Docker images (oven/bun). Neither has a real setup cost for most teams.
Migration cost
| Task | Effort |
|---|---|
| Basic test files (no mocks) | Trivial — same describe/test/expect API |
Replace import from 'vitest' → import from 'bun:test' | Low — search/replace |
Migrate vi.mock() → mock.module() | Medium — different hoisting model, --preload needed |
Migrate __mocks__ directory | High / Blocker — unsupported, manual refactor required |
Migrate @vitest/coverage-v8 | Low — flag change, LCOV-compatible |
Migrate @vitest/ui | N/A — no Bun equivalent |
| Migrate Istanbul coverage | High — not supported in Bun |
Verdict
Pick Bun test if: new TypeScript project, minimal or no __mocks__ patterns, don’t need @vitest/ui, and cold-start speed matters to your feedback loop. The 11× cold-start gap is real, and Bun 1.3.14 is solid for projects that don’t push its compatibility edges.
Stay on Vitest if: your project has __mocks__ directories (unsupported in Bun — manual refactor required), depends on Istanbul coverage (Cloudflare Workers, Bun runtime environments not covered by Bun’s native coverage), or uses @vitest/ui. Both the __mocks__ gap and the Istanbul gap are documented migration blockers; Vitest’s ecosystem depth is the safer default until Bun resolves them.
If you’re on Vitest and everything works, Bun’s speed gains don’t justify the migration cost unless you’ve measured your test suite and found startup to be the actual bottleneck.
Caveats
Benchmarks are from PkgPulse’s 2026 suite (50 tests, 10 TypeScript files, mocking). Your real-world results depend on test count, transform cost, DOM setup, network calls, and CI machine class. Benchmark your own suite before migrating. Neither Vitest nor Bun is an affiliate partner — no commission involved in this verdict.