Skip to the solution
MediumFrontend11 min read

How do you test Vue components (Vitest and Vue Test Utils)?

461 views

The problem

Question presented to candidate: "We have no tests on our Vue 3 front end. Set up component testing with Vitest and Vue Test Utils, and show me how you would test a component with props and events, one that fetches data, and one that uses the router or a Pinia store."

What a strong answer should cover:

  • Setup: Vitest with @vitejs/plugin-vue (so .vue files compile) and environment: "jsdom" (or happy-dom); Vue Test Utils provides mount.
  • The core API: mount(Component, { props, global }), get / find with data-test selectors, trigger and setValue (both awaited, because the DOM updates on the next tick), text(), emitted().
  • Async: mock the API module with vi.mock, then await flushPromises() before asserting.
  • Plugins: pass a real router on createMemoryHistory or a Pinia via global.plugins; createTestingPinia stubs actions and seeds state.
  • Test behaviour, not implementation: what the user sees and what the component emits, not internal refs; stub only heavy children (global.stubs / shallowMount).

Clarifying questions expected:

  • "Are we after unit-level component tests, or end-to-end tests in a real browser too?"

Code / implementation expected: Yes β€” a Vitest config and two or three tests covering events, async data and a plugin.

  • vue
  • testing
  • vitest
  • vue-test-utils

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

11 min read Β· 10 sections

Read the code

Counter.test.js: props, clicks, emitted events, and why trigger is awaited
import { describe, it, expect } from "vitest";
import { mount } from "@vue/test-utils";
import Counter from "../src/components/Counter.vue";

describe("Counter", () => {
  it("renders the start prop", () => {
    const wrapper = mount(Counter, { props: { start: 5 } });
    expect(wrapper.get('[data-test="count"]').text()).toBe("Count: 5");
  });

  it("increments by step and emits change", async () => {
    const wrapper = mount(Counter, { props: { start: 1, step: 2 } });
    await wrapper.get('[data-test="inc"]').trigger("click"); // await = wait for the DOM update
    await wrapper.get('[data-test="inc"]').trigger("click");
    expect(wrapper.get('[data-test="count"]').text()).toBe("Count: 5");
    expect(wrapper.emitted("change")).toEqual([[3], [5]]);
  });

  it("shows why you must await trigger", () => {
    const wrapper = mount(Counter);
    wrapper.get('[data-test="inc"]').trigger("click"); // not awaited
    expect(wrapper.get('[data-test="count"]').text()).toBe("Count: 0"); // DOM not patched yet
    expect(wrapper.emitted("change")).toEqual([[1]]); // but the handler already ran
  });
});
UserCard.test.js: mocking the API module and flushPromises
import { describe, it, expect, vi } from "vitest";
import { mount, flushPromises } from "@vue/test-utils";
import UserCard from "../src/components/UserCard.vue";
import { fetchUser } from "../src/api";

vi.mock("../src/api", () => ({ fetchUser: vi.fn() })); // no real network in unit tests

describe("UserCard", () => {
  it("shows loading, then the user", async () => {
    vi.mocked(fetchUser).mockResolvedValueOnce({ id: 1, name: "Ada" });
    const wrapper = mount(UserCard, { props: { id: 1 } });
    expect(wrapper.text()).toBe("Loading...");
    await flushPromises(); // let the mocked promise resolve and Vue re-render
    expect(wrapper.text()).toBe("Ada");
    expect(fetchUser).toHaveBeenCalledWith(1);
  });

  it("shows the error message", async () => {
    vi.mocked(fetchUser).mockRejectedValueOnce(new Error("HTTP 500"));
    const wrapper = mount(UserCard, { props: { id: 2 } });
    await flushPromises();
    expect(wrapper.get('[role="alert"]').text()).toBe("HTTP 500");
  });
});

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 54 of 92 in Vue.js

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All Vue.js questions
Up nextMedium

What are slots in Vue?

Continue