Vitest and Vue Test Utils
Set up Vitest in a Vue project, mount a component, click it, and check what a person would see.
What you'll learn
When you finish, you can do these.- 1Choose between unit, component and end-to-end tests
- 2Set up Vitest in a Vue project and run the tests
- 3Click a component with Vue Test Utils and check the page
- 4Fix a missing await and a missing test environment
Picture a counter on a web page: a line that reads Count: 0 and an Add one button. You add a Reset button that appears after the first click and, without noticing, break Add one. Nothing warns you, and you find out when someone using the page does. A test is a small program that clicks the button for you and checks the number, every time you save.
Vitest runs your tests, and Vue Test Utils puts one component on a page in memory so a test can click it and read it. The Code tab holds the counter as Counter.vue, a file of three tests for it, and the config files Vitest needs; by the end you can run those tests in a project of your own. You need one idea from Components and props, in the Beginner track: a component is one .vue file for one piece of a page, and another file can import it.
What to test, and how many
Tests come in three sizes. A unit test calls one function, such as one that formats a price, and checks what it returns; thousands run in seconds. A component test puts one component on a page, clicks and types, and reads what it shows. An end-to-end test opens the whole built app in a real browser and walks through a flow, such as paying for a cart. Each one takes seconds, so you keep a few for the flows that matter. The track's last lesson covers them.
Whatever the size, test what a person sees and does: the text on the page, whether a button is there. Avoid reading a component's own variables. The counter keeps its number in a variable, count, which a test can read as wrapper.vm.count. Rename the variable and that test fails, though the page looks the same; after a few of those, people stop trusting the tests.
Set up Vitest in a Vue project
Vitest needs a few settings to run a Vue component, and create-vue, the tool that sets up a new Vue project, writes them for you. In a terminal, run npm create vue@latest and say yes to Vitest. It writes vitest.config.ts, an example test in src/components/__tests__/ and a test:unit script. The main part of vitest.config.ts, as in the Code tab:
export default mergeConfig(
viteConfig,
defineConfig({
test: {
environment: 'jsdom', // a simulated browser page: document, window, elements
exclude: [...configDefaults.exclude, 'e2e/**'],
},
}),
)
The config does three things:
- It merges in
viteConfig, the settings of Vite, the build tool, fromvite.config.ts. Among them, the Vue plugin,@vitejs/plugin-vue, compiles.vuefiles in tests as in the app. - It sets
environment: 'jsdom'. Vitest runs in Node, which runs JavaScript outside the browser and has nodocument, the browser's object for the page; jsdom adds a simulated one.happy-domis a faster alternative. - It skips the
e2efolder, where end-to-end tests live.
npm run test:unit runs vitest in watch mode: it runs every test, prints Waiting for file changes..., and runs the affected tests again each time you save. Press q to quit. npx vitest run runs once and stops, with an error when a test fails. A CI server, a machine that tests every change, needs that.
Your first component test
A first test puts the counter on a page, clicks Add one and checks the number. The counter is src/components/Counter.vue in the Code tab. Its script keeps count in a ref, a box that holds a value and tells Vue when it changes, starting at 0. Its template:
<template>
<p>Count: {{ count }}</p>
<button type="button" @click="count++">Add one</button>
<button v-if="count > 0" type="button" data-test="reset" @click="count = 0">
Reset
</button>
</template>
The page reads Count: 0 above an Add one button. Reset appears only once the count is above zero. It carries a data-test attribute, a marker for tests to find it by, which a change of style never renames.
The tests sit in src/components/__tests__/Counter.spec.ts; Vitest runs every file whose name ends in .spec.ts or .test.ts. The file imports mount from Vue Test Utils and the counter from Counter.vue, and its describe() groups three tests, each an it() with a name. The test counts a click follows the three steps of most tests: arrange, act, assert.
it('counts a click', async () => {
// Arrange: put the component on a page
const wrapper = mount(Counter)
// Act: click, and wait for Vue to update the page
await wrapper.get('button').trigger('click')
// Assert: check what a person would see
expect(wrapper.get('p').text()).toBe('Count: 1')
})
mount(Counter) puts the counter on a page and returns a wrapper, an object for finding elements and reading them. get('button') returns the first element that matches a CSS selector, here the tag name button: the Add one button. trigger('click') clicks it and returns a promise, a value that stands for work still in progress: here, Vue updating the page. await waits for that update, which is why the test's function is async. Then expect() and toBe() check that the paragraph's text is Count: 1, so the test passes.
Ask get() for the Reset button before any click, and it fails the test at once with a message that prints the component's HTML, starting Unable to get [data-test="reset"] within: <p>Count: 0</p>. To prove that something is absent, use find(): it returns an empty wrapper, whose exists() is false. The first test, starts at zero, with no Reset button, does that:
expect(wrapper.find('[data-test="reset"]').exists()).toBe(false)
It passes while the page has no Reset button.
What goes wrong
Forgetting await before trigger() is the most common mistake. Vue does not change the page the moment count changes: it waits a moment and applies all changes together. Without await, the expect() line reads the paragraph before that update.
wrapper.get('button').trigger('click')
expect(wrapper.get('p').text()).toBe('Count: 1')
Vitest prints AssertionError: expected 'Count: 0' to be 'Count: 1' // Object.is equality, and its diff shows Expected "Count: 1" and Received "Count: 0". The click did happen; the test looked too early. Put await back:
await wrapper.get('button').trigger('click')
expect(wrapper.get('p').text()).toBe('Count: 1')
The test waits for the update and passes.
The second mistake, a config without a test environment, happens when you write vitest.config.ts by hand:
defineConfig({
test: {
exclude: [...configDefaults.exclude, 'e2e/**'],
},
})
Vitest then defaults to node, so every test fails on mount() with ReferenceError: document is not defined. Add the environment line:
defineConfig({
test: {
environment: 'jsdom', // a simulated browser page: document, window, elements
exclude: [...configDefaults.exclude, 'e2e/**'],
},
})
Every test gets its simulated page back.
Without the Vue plugin, which create-vue's config merges in, Vitest stops before any test runs: Install @vitejs/plugin-vue to handle .vue files.
What to remember
- Write many unit and component tests and a few end-to-end ones, and in all of them check what a person sees, not
wrapper.vm. - Vitest needs
@vitejs/plugin-vueto read.vuefiles andenvironment: 'jsdom'or'happy-dom'to have adocument. - A test arranges with
mount(), acts withtrigger(), and asserts on what the wrapper shows, such astext()orexists(). get()fails loudly when an element is missing; usefind().exists()only to prove something is not there.- Always
awaitan action that changes the page before you assert.
In your project
Do these in a real project. Each one says how to confirm it worked.- In a project made with
npm create vue@latestand Vitest, addsrc/components/Counter.vueand its test, then runnpm run test:unit. It worked if the terminal showsTests 3 passed (3)and thenWaiting for file changes.... - Leave watch mode running and change
Count:toClicks:inCounter.vue. When you save, all three tests fail, each with Expected "Count: …" and Received "Clicks: …". Change it back and they pass. Then delete theawaitin front of the firsttrigger()incounts a click: the diff reads Expected "Count: 1", Received "Count: 0". - Remove the
environmentline fromvitest.config.tsand runnpx vitest run. Every test fails withReferenceError: document is not defined. Runnpm i -D happy-dom, addenvironment: 'happy-dom', and the three tests pass.