# Testing Vue Apps

Course: [Vue.js](https://www.devtutorials.ai/learn/vue) · Section: Production Vue
Source: https://www.devtutorials.ai/learn/vue/production/testing-vue-apps

> 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.

const props = defineProps({ amount: { type: Number, required: true } });
const text = "₹" + props.amount.toFixed(2);
</script>

<template>
    <span class="price">{{ text }}</span>
</template>
`,
    "PriceTag.spec.ts": `import { describe, it, expect } from "vitest";

describe("PriceTag", () => {
    it("shows rupees with two decimals", () => {
      const wrapper = mount(PriceTag, { props: { amount: 90 } });
      expect(wrapper.text()).toBe("₹90.00");
    });

    it("puts the price in a .price element", () => {
      const wrapper = mount(PriceTag, { props: { amount: 5.5 } });
      expect(wrapper.find(".price").exists()).toBe(true);
    });
});
`,
    "main.ts": `import { run } from "vitest";

run();
`,
  }}
/>

- **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.

<figure className="vue-fig ngc-fig">
<svg viewBox="0 0 640 200" role="img" aria-labelledby="fig-vue-tests" style={{ width: "100%", maxWidth: 640, height: "auto" }}>
  <title id="fig-vue-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</title>
  <g style={{ fontFamily: "var(--mono)", fontSize: 12, fontWeight: 700 }}>
    <rect x="10" y="40" width="110" height="44" rx="9" style={{ fill: "var(--ngc-blue-soft)", stroke: "var(--ngc-blue)" }}><animate attributeName="stroke-width" values="4;1;1;1;1;4" keyTimes="0;0.2;0.4;0.6;0.8;1" dur="7s" repeatCount="indefinite" /></rect><text x="65" y="67" textAnchor="middle" style={{ fill: "var(--ngc-blue)" }}>mount()</text>
    <rect x="135" y="40" width="110" height="44" rx="9" style={{ fill: "var(--ngc-purple-soft)", stroke: "var(--ngc-purple)" }}><animate attributeName="stroke-width" values="1;4;1;1;1;1" keyTimes="0;0.2;0.4;0.6;0.8;1" dur="7s" repeatCount="indefinite" /></rect><text x="190" y="67" textAnchor="middle" style={{ fill: "var(--ngc-purple)" }}>find()</text>
    <rect x="260" y="40" width="110" height="44" rx="9" style={{ fill: "var(--ngc-orange-soft)", stroke: "var(--ngc-orange)" }}><animate attributeName="stroke-width" values="1;1;4;1;1;1" keyTimes="0;0.2;0.4;0.6;0.8;1" dur="7s" repeatCount="indefinite" /></rect><text x="315" y="67" textAnchor="middle" style={{ fill: "var(--ngc-orange)" }}>trigger()</text>
    <rect x="385" y="40" width="110" height="44" rx="9" style={{ fill: "var(--ngc-orange-soft)", stroke: "var(--ngc-orange)" }}><animate attributeName="stroke-width" values="1;1;1;4;1;1" keyTimes="0;0.2;0.4;0.6;0.8;1" dur="7s" repeatCount="indefinite" /></rect><text x="440" y="67" textAnchor="middle" style={{ fill: "var(--ngc-orange)" }}>await</text>
    <rect x="510" y="40" width="120" height="44" rx="9" style={{ fill: "var(--ngc-green-soft)", stroke: "var(--ngc-green)" }}><animate attributeName="stroke-width" values="1;1;1;1;4;1" keyTimes="0;0.2;0.4;0.6;0.8;1" dur="7s" repeatCount="indefinite" /></rect><text x="570" y="67" textAnchor="middle" style={{ fill: "var(--ngc-green)" }}>expect()</text>
  </g>
  <path d="M120 62 H133 M245 62 H258 M370 62 H383 M495 62 H508" style={{ stroke: "var(--ngc-slate)", strokeWidth: 2 }} />
  <text x="320" y="130" textAnchor="middle" style={{ fill: "var(--ngc-slate)", fontFamily: "var(--sans)", fontSize: 13 }}>Arrange (mount) → Act (find, trigger, await) → Assert (expect)</text>
  <text x="320" y="165" textAnchor="middle" opacity="0" style={{ fill: "var(--ngc-green)", fontFamily: "var(--sans)", fontSize: 15, fontWeight: 700 }}><animate attributeName="opacity" values="0;0;1;1" keyTimes="0;0.82;0.85;1" dur="7s" repeatCount="indefinite" />✓ DishCard › adds the dish</text>
</svg>
<figcaption>Most component tests follow the same three steps.</figcaption>
</figure>

## 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.

const props = defineProps({ name: { type: String, required: true } });
const emit = defineEmits(["add"]);
const added = ref<boolean>(false);

function add(): void {
    added.value = true;
    emit("add", props.name);
}
</script>

<template>
    <div>
      <h3>{{ name }}</h3>
      <button :disabled="added" @click="add">{{ added ? "Added" : "Add" }}</button>
    </div>
</template>
`,
    "DishCard.spec.ts": `import { describe, it, expect } from "vitest";

describe("DishCard", () => {
    it("shows the dish name", () => {
      const wrapper = mount(DishCard, { props: { name: "Idli" } });
      expect(wrapper.find("h3").text()).toBe("Idli");
    });

    it("adds the dish when clicked", async () => {
      const wrapper = mount(DishCard, { props: { name: "Idli" } });
      await wrapper.find("button").trigger("click");
      expect(wrapper.find("button").text()).toBe("Added");
      expect(wrapper.find("button").attributes("disabled")).toBeDefined();
      expect(wrapper.emitted("add")).toEqual([["Idli"]]);
    });
});
`,
    "main.ts": `import { run } from "vitest";

run();
`,
  }}
/>

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.

const name = ref<string>("");
const valid = computed<boolean>(() => name.value.trim().length >= 2);
const emit = defineEmits(["book"]);
</script>

<template>
    <form @submit.prevent="emit('book', name)">
      <input v-model="name" />
      <button :disabled="!valid">Book</button>
    </form>
</template>
`,
    "BookingForm.spec.ts": `import { describe, it, expect } from "vitest";

describe("BookingForm", () => {
    it("is disabled until a name is typed", async () => {
      const wrapper = mount(BookingForm);
      expect(wrapper.find("button").attributes("disabled")).toBeDefined();
      await wrapper.find("input").setValue("Asha");
      expect(wrapper.find("button").attributes("disabled")).toBe(undefined);
    });

    it("emits book with the name on submit", async () => {
      const wrapper = mount(BookingForm);
      await wrapper.find("input").setValue("Ravi");
      await wrapper.find("form").trigger("submit");
      expect(wrapper.emitted("book")).toEqual([["Ravi"]]);
    });
});
`,
    "main.ts": `import { run } from "vitest";

run();
`,
  }}
/>

**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.

const names = ref<string[]>([]);
const error = ref<string>("");

onMounted(async () => {
    const res = await fetch("/api/dishes");
    if (!res.ok) {
      error.value = "Could not load";
      return;
    }
    names.value = ((await res.json()) as { name: string }[]).map((d) => d.name);
});
</script>

<template>
    <p v-if="error" class="error">{{ error }}</p>
    <ul v-else><li v-for="n in names" :key="n">{{ n }}</li></ul>
</template>
`,
    "MenuList.spec.ts": `import { describe, it, expect, vi } from "vitest";

describe("MenuList", () => {
    it("shows the dishes from the server", async () => {
      const fakeFetch = vi.fn().mockResolvedValue({
        ok: true,
        json: async () => [{ name: "Dosa" }, { name: "Idli" }],
      });
      window.fetch = fakeFetch;
      const wrapper = mount(MenuList);
      await flushPromises();
      expect(fakeFetch).toHaveBeenCalledWith("/api/dishes");
      expect(wrapper.findAll("li")).toHaveLength(2);
    });

    it("shows an error when the server fails", async () => {
      window.fetch = vi.fn().mockResolvedValue({ ok: false, json: async () => ({}) });
      const wrapper = mount(MenuList);
      await flushPromises();
      expect(wrapper.find(".error").text()).toBe("Could not load");
    });
});
`,
    "main.ts": `import { run } from "vitest";

run();
`,
  }}
/>

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".

```ts
// 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

<div className="tscroll">

| Mistake | Wrong idea | Correct idea | What goes wrong |
|---|---|---|---|
| No `await` after `trigger` | The DOM updates at once | `await wrapper.find(...).trigger(...)` | Checks read the old page |
| Testing private details | Test every internal variable | Test what the user sees | Tests break on every refactor |
| One store shared by all tests | Tests are independent anyway | Fresh Pinia in `beforeEach` | Tests pass or fail depending on order |
| Calling the real server | Real data is more honest | Mock `fetch` | Slow, flaky tests |
| Checking before Promises finish | `mount` waits for data | `await flushPromises()` | Empty lists in assertions |
| `toBe` for arrays | `toBe` compares contents | Use `toEqual` | Equal-looking arrays fail |

</div>

## 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**
