Adding a test runner to a frontend that has zero tests is a different problem than adding one to a codebase that already has hundreds — there's no existing suite to run through the new tool, no confirmation that "everything still passes" is even meaningful yet. That's exactly where Tabeer.ai's Nuxt frontend was before this: real production code, real users, zero unit tests. Here's what actually got set up, and why one real test beats a padded-out placeholder suite.
Install
npm install -D vitest @vue/test-utils @nuxt/test-utils happy-dom
Config
// vitest.config.ts
import { fileURLToPath } from 'node:url'
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
environment: 'happy-dom',
include: ['tests/unit/**/*.spec.ts'],
},
resolve: {
alias: {
'~': fileURLToPath(new URL('./app', import.meta.url)),
'@': fileURLToPath(new URL('./app', import.meta.url)),
},
},
})
Two decisions worth explaining. happy-dom over jsdom: a lighter, faster DOM
implementation for tests that don't need jsdom's more complete (and slower) browser API
surface — the right default until a specific test actually needs something happy-dom doesn't
implement. A plain vitest.config.ts rather than @nuxt/test-utils's full Nuxt test
environment: @nuxt/test-utils spins up an actual Nuxt instance per test file, which is the
right tool for testing components that lean on Nuxt-specific runtime features (auto-imports,
useFetch, route composables) but meaningfully slower for testing plain logic that doesn't
need any of that.
The One Real Test
Not a placeholder — an actual regression test for the exam data every page on the site depends on:
// tests/unit/exams.spec.ts
import { describe, expect, it } from 'vitest'
import { EXAMS } from '~/data/exams'
describe('EXAMS data', () => {
it('has exactly the 8 exams the platform supports', () => {
expect(EXAMS).toHaveLength(8)
})
it('has a unique code and slug per exam', () => {
const codes = new Set(EXAMS.map((e) => e.code))
const slugs = new Set(EXAMS.map((e) => e.slug))
expect(codes.size).toBe(EXAMS.length)
expect(slugs.size).toBe(EXAMS.length)
})
it('every exam has non-empty labels', () => {
for (const exam of EXAMS) {
expect(exam.label.length).toBeGreaterThan(0)
expect(exam.shortLabel.length).toBeGreaterThan(0)
}
})
})
npx vitest run
# Test Files 1 passed (1)
# Tests 3 passed (3)
# Duration 241ms
This is a genuinely useful test, not a token one: this exact data array is what every exam page's routing, subject filtering, and personalization logic reads from — a duplicate slug or a typo'd code here would silently break navigation across the entire site, and this catches it in 241ms instead of during a manual click-through.
Why Not Write a Bigger Suite Right Away
The temptation with a from-zero test setup is to write broad coverage immediately — comprehensive component tests, mocked API responses, the works. That's a real, separate undertaking with its own scope, and cramming it into "set up the test runner" produces exactly the kind of rushed, shallow coverage that looks good in a PR diff and catches nothing real six months later. One test that actually protects something real (data integrity every page depends on) is worth more than twenty tests that assert a component renders without checking whether it renders correctly.
What's Actually in Place Now
npm run test:unit runs in CI-friendly mode (vitest run, not watch mode) in under 300ms. The
next real test to add is whatever the next bug report turns out to be — write the test that
would have caught it, not a test for its own sake.
If you're adding a first test suite to a frontend that's been running in production without one, and want the foundation built right rather than padded for a coverage percentage, get in touch.
By Shahid Malik