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

Install Vitest and Vue Test Utils

If not included by create-vue:

bash
npm install -D vitest @vue/test-utils jsdom @vitest/coverage-v8

vite.config.ts:

typescript
/// <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:

json
"test": "vitest",
"test:run": "vitest run"

Run:

bash
npm run test
npm run test:run

Compare backend testing in Testing FastAPI Applications.

First Component Test

src/components/HelloBanner.vue:

vue
<script setup lang="ts">
defineProps<{ title: string }>();
</script>
 
<template>
  <h1>{{ title }}</h1>
</template>

src/components/__tests__/HelloBanner.spec.ts:

typescript
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:

  • mount renders component in jsdom
  • wrapper.text() gets visible text content
  • Pass props like a parent would

Trigger Events

CounterButton.vue:

vue
<script setup lang="ts">
import { ref } from "vue";
const count = ref(0);
</script>
 
<template>
  <button @click="count++">Count: {{ count }}</button>
</template>

Test:

typescript
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

typescript
const wrapper = mount(SaveButton);
await wrapper.find("button").trigger("click");
expect(wrapper.emitted("save")).toHaveLength(1);

find vs get

findget
Missing elementReturns empty wrapperThrows
UseOptional UIMust exist

Test Composables

Pure composables without lifecycle:

typescript
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:

typescript
expect(fetch).toHaveBeenCalledWith(expect.stringContaining("/posts"));

Router in Tests

Snapshot Tests (Sparingly)

typescript
expect(wrapper.html()).toMatchSnapshot();

Use for stable presentational markup—avoid huge snapshots that break on every CSS class tweak.

What to Test

TestSkip
Props → rendered outputFramework internals
Emits on user actionThird-party library internals
Store actions / gettersEvery trivial wrapper
Composable logicFull E2E in unit tests

E2E with Playwright or Cypress covers full flows—concept only in this track.

Practice Exercise

  1. Test a TodoItem component: checkbox toggles emit
  2. Test useCounter composable
  3. Mock fetch in 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.