I made my Vitest suite in Nuxt run ten times faster
Source: Dev.to
Why Automated Tests Matter
- Safety net: Prevent regressions when the codebase grows.
- Confidence: New features can be added and verified quickly.
- Speed vs. reliability: More tests = longer runs, which can become a pain point.
My Nuxt module for Neon DB connections originally took 180 + seconds to run. That latency killed my developer experience (DevEx) and forced me to rethink the test architecture.
The Original (Slow) Setup
-
One Nuxt app per test file – each file created its own isolated Nuxt instance.
-
Sequential execution – tests ran one after another, so every boot‑up delay added up.
-
E2E helpers – each file called
await setup({ rootDir: fileURLToPath(new URL('./neon-test-app', import.meta.url)), })which instantiated a fresh Nuxt app every time.
The result? ~3 minutes of waiting before any feedback.
The First Improvement – Merge Test Apps
I combined the separate test apps into a single Nuxt app:
- Each test still had its own page (different URL) that performed a SQL action via a button.
- I squashed the multiple
app/pagesdirectories into one and tweakedapp.vue.
Result: Roughly 2× faster because the same app was reused, but the suite was still far from optimal.
The Real Fix – Share a Single Nuxt Instance Across All Tests
Goal
When any test does:
const page = await createPage()
it should reuse the same underlying Nuxt instance that was started only once.
Solution Overview
- Global setup – mount the Nuxt app once before Vitest starts any test.
- Shared test context – expose the same Playwright browser (or virtual browser) to every test file.
- Environment variable – tell
@nuxt/test-utilswhich app to mount.
1️⃣ Add a Global Setup File
vitest.config.ts
export default defineConfig({
// …other config
globalSetup: ['./node_modules/@nuxt/test-utils/dist/runtime/global-setup.mjs'],
})
This file runs once before any Vitest worker starts. It mounts the Nuxt app in an emulated browser.
2️⃣ Configure the App to Mount
Create (or edit) a small script that sets NUXT_TEST_OPTIONS:
// vitest.global-setup.ts (or any file you import via globalSetup)
import { resolve, fileURLToPath, URL } from 'node:path'
const rootDir = resolve(
fileURLToPath(new URL('.', import.meta.url)),
'test/neon-test-app'
)
// Used by @nuxt/test-utils/runtime/global-setup
process.env.NUXT_TEST_OPTIONS = JSON.stringify({
// Path to the test app
rootDir,
// Do NOT create a Playwright browser here – we’ll do it lazily
browser: false,
})
Explanation: @nuxt/test-utils reads NUXT_TEST_OPTIONS during its global‑setup phase and mounts the specified app.
3️⃣ Set Up a Shared Browser Instance
e2e.setup.ts
import { useTestContext, createBrowser } from '@nuxt/test-utils/e2e'
beforeAll(async () => {
const ctx = useTestContext()
// Initialise the virtual browser only once
if (!ctx.browser) {
await createBrowser()
}
})
useTestContext()gives access to the global test context shared across files.createBrowser()spins up a Playwright (or Puppeteer) browser once; subsequent tests reuse it.
4️⃣ Write Tests as Usual
import { createPage } from '@nuxt/test-utils/e2e'
test('my feature works', async () => {
const page = await createPage()
await page.goto('/my-test-page')
// …assertions
})
All tests share the same mounted Nuxt app and the same browser instance, eliminating the per‑file boot cost.
Result
| Before | After |
|---|---|
| ~180 s (3 min) | No, this is not possible. > “This is the moment where I need to be very explicit and honest, because you’ve now hit a hard boundary, not a missing trick.” I found it hilarious later, but at the time it was a harsh truth. I had wasted hours chasing my own shadow, with AI happily assisting and encouraging me to continue. I have fast tests, but they are badly observable: either no output at all, or output littered with unrelated debug messages, or platform‑dependent script commands to polish it. |
The suite now runs in ~20 seconds.
The Idea
What if I stop trying to abuse Vitest into doing something it isn’t designed for?
What if I aggregate my test files into one big suite?
That way the test app would be built and mounted once, and then all tests would run fast. Yes, it would be one large test file with many test cases, but that doesn’t matter from a runtime perspective. I can still keep the source code isolated by having separate definition files that are dynamically imported in a single e2e.test.ts file—the only file executed by Vitest.
The Final Solution
I removed globalSetup and setupFiles from vitest.config.ts. All I need now is a small, clean e2e.test.ts file:
import { fileURLToPath } from 'node:url'
import { setup } from '@nuxt/test-utils/e2e'
// only setup nuxt-test-app ONCE
await setup({
rootDir: fileURLToPath(new URL('../neon-test-app', import.meta.url)),
// Playwright browser is not required for now
browser: false,
})
// import and run E2E test suites AFTER the test app is ready
await import('../neon-test-suites/01-basic')
await import('../neon-test-suites/02-select')
await import('../neon-test-suites/03-insert')
await import('../neon-test-suites/04-update')
await import('../neon-test-suites/05-delete')
The code:
- Prepares the Nuxt test app via the dedicated
setupfunction. - Awaits and executes each suite file after the app is ready.
That’s it—no hacks, no console mess. It works like a charm and is still fast: ~20 seconds and you’re done. The output now remains clean in the console.
Takeaways
- I finally wrestled with Vitest + Nuxt and won.
- If you’d like more details about the implementation or have suggestions for improvement, let me know in the comments.
AI Tools: Helpful but Not Infallible
Both Copilot and ChatGPT resembled a typical developer who throws “should work” solutions without seeing the whole picture. Where they failed—and I eventually succeeded—was the ability to step back and re‑think the entire situation. This gap still exists between human developers and artificial pseudo‑intelligence.
Note: This isn’t an anti‑AI rant. AI helps me daily, and I enjoy its assistance. I just think we need to be aware of its limits. This was another nice lesson for me, and I hope you found it interesting too.