Setting Up Tests
Install Vitest, point it at your Vite config, give it a DOM, and get one component test passing.
vitest run [files] # watch instead: vitestA test runner loads files that assert things about your code, runs them, and reports which assertions held. This course uses Vitest, because it reads the Vite config the project already has.
That matters more than it sounds. A single-file component is not JavaScript. Something has to compile the <template> block into a render function before anything can run it. Vitest reuses Vite's transform pipeline, so the Vue plugin, the @ alias and your environment variables behave in tests the way they behave in the browser.
Setting it up
A project scaffolded with npm create vue@latest can add unit testing for you. It installs vitest, @vue/test-utils and jsdom, writes vitest.config.ts, and adds a test:unit script. To add it to an existing project, install those three yourself and write the config.
jsdom is an implementation of the DOM in JavaScript. Tests run in Node, where there is no document, so environment: 'jsdom' supplies one. Without it, mounting anything fails on a missing document.
Running it
vitest watches your files and re-runs the affected tests on save. vitest run runs once and exits; vitest --run is the same command written the other way. Vitest also switches to run mode on its own when it detects a CI environment, so the script your CI calls and the script you call locally can be the same one.
Example
// src/components/__tests__/HelloBadge.spec.ts
import { describe, expect, it } from 'vitest'
import { mount } from '@vue/test-utils'
import HelloBadge from '../HelloBadge.vue'
describe('HelloBadge', () => {
it('shows the label it is given', () => {
const wrapper = mount(HelloBadge, { props: { label: 'beta' } })
expect(wrapper.text()).toBe('beta')
})
it('renders a single span', () => {
const wrapper = mount(HelloBadge, { props: { label: 'new' } })
expect(wrapper.findAll('span')).toHaveLength(1)
})
})When to use it
- Add a unit test suite to a Vue project that was scaffolded without one.
- Prove a component renders before wiring it into a page.
- Give CI a single command that fails the build when a test fails.
More examples
vitest.config.ts
This is the config create-vue writes. mergeConfig folds your Vite config into the test config, so plugins and aliases carry over instead of being repeated. configDefaults.exclude is Vitest's own ignore list, kept and extended rather than replaced, which keeps end-to-end specs out of the unit run. Reach for this shape whenever you want test settings separate from vite.config.ts; the alternative is a test block inside vite.config.ts itself.
// vitest.config.ts
import { fileURLToPath } from 'node:url'
import { mergeConfig, defineConfig, configDefaults } from 'vitest/config'
import viteConfig from './vite.config'
export default mergeConfig(
viteConfig,
defineConfig({
test: {
environment: 'jsdom',
exclude: [...configDefaults.exclude, 'e2e/**'],
root: fileURLToPath(new URL('./', import.meta.url)),
},
}),
)The component under test
src/components/HelloBadge.vue, the component the first test mounts. It draws a pill containing whatever text the label prop holds. A component with no state of its own is the right first thing to test: if the suite can render this, the compiler, the jsdom environment and the alias resolution are all working, so a later failure is about your code rather than your setup.
<script setup lang="ts">
defineProps<{ label: string }>()
</script>
<template>
<span class="badge">{{ label }}</span>
</template>
<style scoped>
.badge {
padding: 0.125rem 0.5rem;
border-radius: 999px;
background: #eef;
}
</style>The scripts you run
The three dev dependencies a unit suite needs, and the script that runs it. npm run test:unit starts watch mode for local work; npm run test:unit -- --run forces a single pass, which is what you want in a pre-commit hook. type-check is separate on purpose: Vitest strips types rather than checking them, so a test file that would fail vue-tsc still runs.
{
"scripts": {
"dev": "vite",
"build": "vite build",
"type-check": "vue-tsc --build",
"test:unit": "vitest"
},
"devDependencies": {
"@vue/test-utils": "^2.5.0",
"jsdom": "^30.0.1",
"vitest": "^4.1.11"
}
}
Discussion