Testing Vue Apps

By Syncfusion®, Inc. · Last updated

Why and what to test, Vitest basics with describe, it and expect, Vue Test Utils: mount, find, trigger, setValue, text, props and emitted events, awaiting updates, testing composables and Pinia stores, and mocking fetch with vi.fn() and flushPromises().

Why Test?

  • Every change can break something that used to work.
  • Clicking through the whole app after each change is slow, and people forget steps.
  • A test = code that uses your code and checks the result. It runs in seconds.
  • Good tests let you change code without fear.

Simple lesson: tests check your app for you, every time, in seconds.

A Real-World Example

  • Meera's PriceTag shows prices like ₹90.00.
  • A new developer changes the format by mistake. The bill prints 90 instead.
  • A test would have caught it in a second.
  • What happened: both tests passed. The preview shows a report, and the Console lists each test.
  • Change toFixed(2) to toFixed(0) in PriceTag.vue and run again. The first test fails.

In a real project you install Vitest and Vue Test Utils (create-vue offers them), write *.spec.ts files, and run npx vitest. In this lab, a small Vitest look-alike runs in the preview: main.ts imports the spec files and calls run(). The test code itself is the same.

What You Will Learn

  1. What to test and what not to test.
  2. Vitest basics: describe, it and expect.
  3. Mounting a component with Vue Test Utils.
  4. Finding elements and clicking them like a user.
  5. Why you must await after a click.
  6. Typing into inputs and checking emitted events.
  7. Testing composables.
  8. Testing Pinia stores.
  9. Mocking fetch.

What to Test

  • Test what the user sees and does: text on the screen, clicks, typed input, events sent to parents.
  • Do not test how it is built inside, such as private variable names. Then refactors do not break tests.
  • Unit tests check one function, composable or component.
  • Component tests mount a component and use it.
  • End-to-end tests drive a real browser through the whole app, with tools such as Playwright.
  • Most tests should be small and fast unit and component tests.

Animation: a test mounts DishCard, finds the Add button, triggers a click, waits, then checks that the text says Added and that an add event was emitted. Each step lights up in turn

mount() find() trigger() await expect()

Arrange (mount) → Act (find, trigger, await) → Assert (expect) ✓ DishCard › adds the dish

Most component tests follow the same three steps.

Vitest Basics

  • Vitest = a fast test runner made by the Vite team. It understands your Vite setup.
  • describe("name", fn) groups tests.
  • it("does something", fn) (or test) is one test.
  • expect(value).toBe(other) checks a result. If the check fails, the test fails.
  • Common matchers: toBe, toEqual (for objects and arrays), toContain, toHaveLength, toBeTruthy, toThrow (pass a function: expect(() => fn()).toThrow()), and .not.
  • What happened: three tests passed. The last one failed and said why: expected 10 to be 11.

Simple lesson: describe groups, it tests, expect checks.

Vue Test Utils

  • Vue Test Utils (VTU) = the official library for mounting components in tests.
  • mount(Component, { props }) renders it and returns a wrapper.
  • wrapper.find("button") finds an element. .exists() checks it is there.
  • wrapper.text() and wrapper.html() read the output.
  • await wrapper.find("button").trigger("click") clicks it.
  • wrapper.emitted("add") lists the events the component sent.
  • A component that uses the router, Pinia or a plugin needs them in the test: mount(Comp, { global: { plugins: [router, pinia] } }).
  • shallowMount replaces child components with stubs, to test one component alone.
  • Vitest needs a fake browser: environment: "jsdom" (or happy-dom) in its config. create-vue sets this for you.

Always await a trigger() or setValue(). Vue updates the DOM a moment later, as you saw in How Vue Renders. Without await, you check the old page and the test fails, or worse, passes for the wrong reason.

Typing into Inputs

  • await wrapper.find("input").setValue("Asha") types into a box and fires its events.
  • v-model then updates, just as when a person types.

Simple lesson: mount, find, await trigger or setValue, then expect.

Testing Composables and Stores

  • A composable with only refs and computed values can be called straight in a test.
  • One with lifecycle hooks needs a component around it. Mount a tiny test component that uses it.
  • For a Pinia store, create a fresh Pinia before each test with setActivePinia(createPinia()).
  • A fresh Pinia means no state leaks from one test to the next.
  • When mounting a component, createTestingPinia() from @pinia/testing gives a store whose actions are mocks you can check.

Simple lesson: call simple composables directly. Give each store test a fresh Pinia.

Mocking fetch

  • Tests must not depend on a real server. It may be slow, down, or change its data.
  • A mock = a fake function that you control.
  • vi.fn() creates one. .mockResolvedValue(x) makes it return a Promise of x.
  • Replace window.fetch with the mock, then check what was called.
  • flushPromises() from VTU waits until all pending Promises have finished.

Real Vitest has more tools: vi.spyOn, fake timers with vi.useFakeTimers(), afterEach and snapshot tests. This lab's runner has the common ones only: describe, it, test, expect, beforeEach and vi.fn.

Simple lesson: mock fetch with vi.fn(), then await flushPromises() before you check.

End-to-End Tests

  • End-to-end (E2E) test = a program drives a real browser through the whole app: open the page, click, type, check.
  • It catches problems that unit tests miss: routing, real CSS, real network.
  • Playwright is a common choice. create-vue can add it.
  • E2E tests are slower. Keep a few for the most important paths, such as "order a dish".
// e2e/order.spec.ts in a real project (npx playwright test)
import { test, expect } from "@playwright/test";

test("a visitor can order a dosa", async ({ page }) => {
  await page.goto("http://localhost:5173/menu");
  await page.getByRole("button", { name: "Add Masala Dosa" }).click();
  await page.getByRole("link", { name: "Cart" }).click();
  await expect(page.getByText("1 × Masala Dosa")).toBeVisible();
});
  • In this lab, E2E tests cannot run, because they need their own browser. The code above is the real shape.

TL;DR

  • Test = code that uses your code and checks the result.
  • What to test = what users see and do, not private details.
  • Vitest = describe groups, it tests, expect(x).toBe(y) checks.
  • mount(C, { props }) = renders a component and returns a wrapper.
  • find / findAll / text = locate elements and read them.
  • await trigger() / await setValue() = act like a user, then wait for the update.
  • emitted("name") = the events a component sent, with their data.
  • Stores = setActivePinia(createPinia()) before each test.
  • Mocks = vi.fn().mockResolvedValue(x) and flushPromises().

Common mistakes

MistakeWrong ideaCorrect ideaWhat goes wrong
No await after triggerThe DOM updates at onceawait wrapper.find(...).trigger(...)Checks read the old page
Testing private detailsTest every internal variableTest what the user seesTests break on every refactor
One store shared by all testsTests are independent anywayFresh Pinia in beforeEachTests pass or fail depending on order
Calling the real serverReal data is more honestMock fetchSlow, flaky tests
Checking before Promises finishmount waits for dataawait flushPromises()Empty lists in assertions
toBe for arraystoBe compares contentsUse toEqualEqual-looking arrays fail

Check Yourself

Exercises

What to Remember

  • Test what users see and do, not private details.
  • describe, it and expect are the core of Vitest.
  • Mount with VTU, then await every trigger and setValue.
  • Check events with emitted(). Give each store test a fresh Pinia.
  • Mock fetch with vi.fn() and wait with flushPromises().

Next: Performance