Vue cookbook · Beginner
Test components with Vitest and Vue Test Utils
UI changes break forms and buttons without anyone noticing until production.
Approach
Mount components in a DOM environment (happy-dom or jsdom), interact through the rendered output, await DOM
updates, and assert on text, attributes and emitted events. Provide plugins like Pinia in global.plugins.
Solution
// LoginForm.test.ts
import { mount, flushPromises } from "@vue/test-utils";
import { createPinia } from "pinia";
import { describe, expect, it, vi } from "vitest";
import LoginForm from "./LoginForm.vue";
describe("LoginForm", () => {
it("shows an error for a wrong password", async () => {
vi.stubGlobal("fetch", vi.fn().mockResolvedValue(new Response(null, { status: 401 })));
const wrapper = mount(LoginForm, { global: { plugins: [createPinia()] } });
await wrapper.get("input[type=email]").setValue("[email protected]");
await wrapper.get("input[type=password]").setValue("wrong");
await wrapper.get("form").trigger("submit");
await flushPromises();
expect(wrapper.get("[role=alert]").text()).toBe("Invalid email or password");
expect(wrapper.emitted("success")).toBeUndefined();
});
it("emits success after logging in", async () => {
vi.stubGlobal("fetch", vi.fn().mockResolvedValue(Response.json({ name: "Ana" })));
const wrapper = mount(LoginForm, { global: { plugins: [createPinia()] } });
await wrapper.get("input[type=email]").setValue("[email protected]");
await wrapper.get("input[type=password]").setValue("correct-horse");
await wrapper.get("form").trigger("submit");
await flushPromises();
expect(wrapper.emitted("success")?.[0]).toEqual([{ name: "Ana" }]);
});
});
Pitfalls
- Asserting on component internals (
wrapper.vm.state) instead of what users see. - Forgetting
flushPromises()after async work triggered by events. - Snapshot tests of whole pages that change constantly and get updated without review.
Variations
@pinia/testing'screateTestingPiniastubs actions and lets you set initial state.- Use Playwright component or end-to-end tests for flows across pages.
Further reading: Vue Test Utils
Reference: https://test-utils.vuejs.org/guide/
More Vue recipes
- Access child component methods with template refs and defineExpose
- Add SEO metadata and structured data in Nuxt
- Build an accessible modal with Teleport
- Build custom inputs with defineModel and modifiers
- Catch component errors with an error boundary
- Debounce user input with a custom ref
- Fetch data in a composable and cancel stale requests
- Fetch, refresh and share data with Nuxt useFetch