Testing Vue Components
Introduction
Vitest runs fast unit tests in the same Vite toolchain as your app. Vue Test Utils mounts components in jsdom, simulates clicks, and asserts rendered output. This chapter sets up tests for components, composables, Pinia stores, and mocked fetch—without starting a real browser for every case.
Prerequisites
- Components: Props and Emits
- Composables
- create-vue project with Vitest enabled (or manual install below)
Install Vitest and Vue Test Utils
If not included by create-vue:
npm install -D vitest @vue/test-utils jsdom @vitest/coverage-v8vite.config.ts:
/// <reference types="vitest/config" />
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
export default defineConfig({
plugins: [vue()],
test: {
environment: "jsdom",
globals: true,
},
});package.json script:
"test": "vitest",
"test:run": "vitest run"Run:
npm run test
npm run test:runCompare backend testing in Testing FastAPI Applications.
First Component Test
src/components/HelloBanner.vue:
<script setup lang="ts">
defineProps<{ title: string }>();
</script>
<template>
<h1>{{ title }}</h1>
</template>src/components/__tests__/HelloBanner.spec.ts:
import { describe, it, expect } from "vitest";
import { mount } from "@vue/test-utils";
import HelloBanner from "../HelloBanner.vue";
describe("HelloBanner", () => {
it("renders title prop", () => {
const wrapper = mount(HelloBanner, {
props: { title: "Hello Test" },
});
expect(wrapper.text()).toContain("Hello Test");
});
});Code explanation:
mountrenders component in jsdomwrapper.text()gets visible text content- Pass
propslike a parent would
Trigger Events
CounterButton.vue:
<script setup lang="ts">
import { ref } from "vue";
const count = ref(0);
</script>
<template>
<button @click="count++">Count: {{ count }}</button>
</template>Test:
import { mount } from "@vue/test-utils";
import CounterButton from "../CounterButton.vue";
it("increments on click", async () => {
const wrapper = mount(CounterButton);
await wrapper.find("button").trigger("click");
expect(wrapper.text()).toContain("Count: 1");
});Use await after trigger when updates are async.
Testing Emits
const wrapper = mount(SaveButton);
await wrapper.find("button").trigger("click");
expect(wrapper.emitted("save")).toHaveLength(1);find vs get
find | get | |
|---|---|---|
| Missing element | Returns empty wrapper | Throws |
| Use | Optional UI | Must exist |
Test Composables
Pure composables without lifecycle:
import { useCounter } from "@/composables/useCounter";
it("increments", () => {
const { count, increment } = useCounter(0);
increment();
expect(count.value).toBe(1);
});Composables with onMounted, use test helper:
Pinia in Tests
Mock fetch
Assert call:
expect(fetch).toHaveBeenCalledWith(expect.stringContaining("/posts"));Router in Tests
Snapshot Tests (Sparingly)
expect(wrapper.html()).toMatchSnapshot();Use for stable presentational markup—avoid huge snapshots that break on every CSS class tweak.
What to Test
| Test | Skip |
|---|---|
| Props → rendered output | Framework internals |
| Emits on user action | Third-party library internals |
| Store actions / getters | Every trivial wrapper |
| Composable logic | Full E2E in unit tests |
E2E with Playwright or Cypress covers full flows—concept only in this track.
Practice Exercise
- Test a
TodoItemcomponent: checkbox toggles emit - Test
useCountercomposable - Mock
fetchin a store action test
FAQ
Vitest vs Jest?
Vitest integrates with Vite config—default for Vue 3 projects.
Test .vue files?
Import SFCs directly—Vite plugin handles them in Vitest.
@testing-library/vue?
Alternative query style—Vue Test Utils is common in Vue docs.
CI?
vitest run in GitHub Actions—Vue Docker and CI.
Flaky tests?
Avoid real timers without vi.useFakeTimers(); await DOM updates.
Coverage?
vitest run --coverage with @vitest/coverage-v8.