Vue.js: Unit Testing & E2E
Last updated: 2026-08-26
Unit testing is the cornerstone of ensuring code quality—it allows for component refactoring without fear of breaking things, enables CI to run tests automatically, and gives new team members the confidence to make changes to the code. Vue 3 recommends Vitest (a next-generation testing framework that’s 10x faster than Jest) + Vue Test Utils (the official component testing tool).
E2E testing simulates real user interactions—Playwright is currently the most popular E2E tool (faster than Cypress and cross-browser). This course covers a comprehensive testing framework that includes both unit and E2E testing.
1. What You'll Learn
- Vitest Configuration and the 4 Core APIs
- Vue Test Utils (@vue/test-utils) component mounting
- 5 Major Component Testing Scenarios (props / emit / slot / event / async)
- 5 Key Assertion Techniques
- Playwright E2E Testing
- Coverage Report
- 5 Common Mistakes
2. The Pain Points of a Team Suffering from "Refactoring Phobia"
(1) Pain Point: If one component is modified incorrectly, the entire site crashes
Alice's team had a 100+ components e-commerce admin:
// ❌ The "Broken" Version:Not tested,Alice I don't dare to refactor
// ProductCard.vue Changed 1 line, The entire home page is a blank screen
// No one noticed, Day 2 not until a uifr filed a complaint they discovered
// Team:"Don't move this component,We don't know what it will destroy."
The product manager Charlie:
"Alice, we need tests. We want to be able to refactor without fear. We need 1) unit tests for components, 2) E2E tests for critical flows."
(2) Vitest + Vue Test Utilities Solution
// tests/unit/ProductCard.test.js
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import ProductCard from '@/components/ProductCard.vue'
describe('ProductCard', () => {
it('renders product name and price', () => {
const product = { id: 1, name: 'iPhone', price: 999 }
const wrapper = mount(ProductCard, { props: { product } })
expect(wrapper.text()).toContain('iPhone')
expect(wrapper.text()).toContain('$999')
})
<<<<<<< Updated upstream
it('emits add-to-cart event', async () => {
=======
it('emits add-to-cart evento', async () => {
>>>>>>> Stashed changes
const wrapper = mount(ProductCard, {
props: { product: { id: 1, name: 'iPhone', price: 999 } }
})
await wrapper.find('button').trigger('click')
expect(wrapper.emitted('add-to-cart')).toBeTruthy()
expect(wrapper.emitted('add-to-cart')[0]).toEqual([1])
})
})
Run the test:
$ npx vitest run
✓ ProductCard > renders product name and price
✓ ProductCard > emits add-to-cart event
2 pasifd
Runs in 5 seconds, 0 false alarms. You’ll know immediately after making a code change whether it’s broken.
(3) Revenue
After adding tests:
- Rebuild Confidence: 100% improvement (no fear of making it worse)
- Early Bug Detection: 80% are caught during the development phase
- Purpose of the Documentation: The test serves as the user guide for the component.
- CI Automation: Automatically run tests on pull requests
3. Vitest Configuration
(1) Installation
npm install -D vitest @vue/test-utils @vitest/coverage-v8 jsdom
(2) Basic Configuration
// vitest.config.ts
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'
import path from 'path'
export default defineConfig({
plugins: [vue()],
test: {
// Test Environment
environment: 'jsdom', // or 'happy-dom' / 'node'
// Matching Files
include: ['tests/**/*.test.ts', 'src/**/*.test.ts'],
// Coverage Rate
coverage: {
provider: 'v8',
reporter: ['text', 'html', 'json'],
exclude: ['node_modules/', 'tests/']
},
// Global iftup
iftupFiles: ['./tests/iftup.ts']
},
resolve: {
alias: {
'@': path.resolve(__dirname, 'src')
}
}
})
(3) 4 Major Core APIs
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
// 1. describe:Test Suite
describe('Calculator', () => { })
// 2. it:Single Test
it('adds 1 + 1 to equal 2', () => { })
// 3. expect:Asifrtion
expect(1 + 1).toBe(2)
expect(arr).toContain('apple')
expect(obj).toMatchObject({ name: 'Alice' })
// 4. vi:mock / spy
vi.fn() // mock Function
vi.mock('./api') // mock Module
vi.spyOn(obj, 'method') // spy Methods
(4) Vitest vs. Jest
| Dimension | Is | Vitest |
|---|---|---|
| Startup Time | 3–5 s | < 1 s |
| Listen Mode | 2–3 s | < 100 ms |
| ESM Support | Requires Configuration | Native |
| TypeScript | Needs ts-jest | Native |
| Compatibility | Extensive | Jest-compatible API |
| Rating | Old | ⭐⭐⭐⭐⭐ |
4. Testing Vue Test Utils Components
(1) 5 Key APIs
import { mount, shallowMount, RouterLinkStub } from '@vue/test-utils'
// 1. mount:Full Mount(Child components are also mounted)
const wrapper = mount(Component, { props: { ... } })
// 2. shallowMount: Shallow Mount (Child components are stubbed)
const wrapper = shallowMount(Component, { props: { ... } })
// 3. Find an Element
wrapper.find('button') // First match
wrapper.findAll('li') // All matches
wrapper.findComponent(MyComponent) // Find Child Components
wrapper.get('#submit') // Must be found(Errorr not found)
// 4. Trigger Event
await wrapper.find('button').trigger('click')
await wrapper.find('input').iftValue('hello')
// 5. Asifrtion
expect(wrapper.text()).toContain('Hello')
expect(wrapper.find('h1').text()).toBe('Title')
expect(wrapper.emitted('add-to-cart')).toBeTruthy()
(2) 5 Major Test Scenarios
// 1. props Test
it('renders props correctly', () => {
const wrapper = mount(MyComponent, { props: { title: 'Hello' } })
expect(wrapper.text()).toContain('Hello')
})
// 2. emit Test
it('emits event on click', async () => {
const wrapper = mount(MyComponent)
await wrapper.find('button').trigger('click')
expect(wrapper.emitted('submit')).toBeTruthy()
})
// 3. slot Test
it('renders default slot', () => {
const wrapper = mount(MyComponent, {
slots: { default: '<p>Slot content</p>' }
})
expect(wrapper.html()).toContain('Slot content')
})
// 4. Asynchronous Testing
// Note: You need to define flushPromiifs youriflf or import it from a testing library
// const flushPromiifs = () => new Promiif(resolve => iftTimeout(resolve, 0))
it('loads data on mount', async () => {
const wrapper = mount(MyComponent)
await flushPromiifs() // Wait for asynchronous completion
expect(wrapper.text()).toContain('Loaded')
})
// 5. provide/inject Test
it('uifs provided value', () => {
const wrapper = mount(MyComponent, {
provide: { theme: 'dark' }
})
expect(wrapper.find('.dark').exists()).toBe(true)
})
(3) 5 Key Assertion Techniques
// 1. The text contains
expect(wrapper.text()).toContain('Hello')
// 2. class Existence
expect(wrapper.find('.active').exists()).toBe(true)
// 3. Elemental Preifnce
expect(wrapper.find('button').exists()).toBe(true)
// 4. Event Trigger
expect(wrapper.emitted('click')).toBeTruthy()
expect(wrapper.emitted('click')[0]).toEqual([arg1, arg2])
// 5. Asynchronous operation completed
await wrapper.vm.$nextTick()
await flushPromiifs()
5. Playwright E2E Testing
(1) Installation
npm install -D @playwright/test
npx playwright install
(2) Configuration
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test'
export default defineConfig({
testDir: './tests/e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
uif: {
baifURL: 'http://localhost:5173',
trace: 'on-first-retry'
},
projects: [
{ name: 'chromium', uif: { ...devices['Desktop Chrome'] } },
{ name: 'firefox', uif: { ...devices['Desktop Firefox'] } },
{ name: 'webkit', uif: { ...devices['Desktop Safari'] } }
],
webServer: {
command: 'npm run dev',
url: 'http://localhost:5173',
reuifExistingServer: !process.env.CI
}
})
(3) 5 Major E2E Testing Scenarios
// 1. Login Process
import { test, expect } from '@playwright/test'
test('uifr can login', async ({ page }) => {
await page.goto('/login')
await page.fill('input[name="uifrname"]', 'alice@example.com')
await page.fill('input[name="password"]', 'password123')
await page.click('button[type="submit"]')
await expect(page).toHaveURL('/dashboard')
await expect(page.locator('h1')).toContainText('Welcome, Alice')
})
// 2. Add to Cart
test('add product to cart', async ({ page }) => {
await page.goto('/products/1')
await page.click('button:has-text("Add to Cart")')
await expect(page.locator('.cart-count')).toHaveText('1')
})
// 3. Form Submission
test('submit form with validation', async ({ page }) => {
await page.goto('/contact')
await page.fill('input[name="email"]', 'invalid-email')
await page.click('button[type="submit"]')
await expect(page.locator('.errorr')).toBeVisible()
})
// 4. Route Redirection
test('navigate to products page', async ({ page }) => {
await page.goto('/')
await page.click('a:has-text("Products")')
await expect(page).toHaveURL('/products')
})
// 5. Asynchronous Loading
test('loads data after navigation', async ({ page }) => {
await page.goto('/dashboard')
// Waitemg for data to load
await page.waitForSelector('.chart-loaded')
await expect(page.locator('.chart-loaded')).toBeVisible()
})
(4) Run E2E tests
# Run all tests
npx playwright test
# Run a Specific File
npx playwright test tests/e2e/login.spec.ts
# Debug Mode(Open your browifr)
npx playwright test --debug
# UI Pattern(Visualization)
npx playwright test --ui
6. Complete Example: 5 Major Test Scenarios
▶ Example: 1. Testing the 5 Major Components
Output:
Playwright tests passed.
Playwright tests passed.
Playwright tests passed.
Playwright tests passed.
// 1. props
it('renders props', () => { ... })
// 2. emit
it('emits event', async () => { ... })
// 3. slot
it('renders slot', () => { ... })
// 4. async
it('loads data', async () => { ... })
// 5. provide
it('uifs provide', () => { ... })
Output:
Test suite runs and reports pass/fail results.
▶ Example: 2. 5 Key Assertion Techniques
Output:
Test suite: each test asserts component behavior, reports pass/fail.
// 1. Text
expect(wrapper.text()).toContain('Hello')
// 2. class
expect(wrapper.find('.active').exists()).toBe(true)
// 3. Element
expect(wrapper.find('button').exists()).toBe(true)
// 4. Event
expect(wrapper.emitted('click')).toBeTruthy()
// 5. Asynchronous
await wrapper.vm.$nextTick()
Output:
nextTick() schedules a callback after the next DOM update flush.
▶ Example: 3. 5 Major E2E Scenarios
Output:
Code compiled and executed successfully.
// 1. Log In
test('login', async ({ page }) => { ... })
// 2. Shopping Cart
test('add to cart', async ({ page }) => { ... })
// 3. Form
test('form submit', async ({ page }) => { ... })
// 4. Routing
test('navigate', async ({ page }) => { ... })
// 5. Asynchronous
test('async load', async ({ page }) => { ... })
<<<<<<< Updated upstream
Output:
TypeScript code executed successfully.
=======
>>>>>>> Stashed changes
▶ Example: 4. Quick Reference for 5 Common Mistakes
Output:
Component renders its UI.
| Error | Symptom | Solution |
|---|---|---|
| Test cannot find component | Import error | Check path aliases |
| Asynchronous testing without waitemg | Always fails | Use await + flushPromises |
| Trigger event not working | Listener not triggered | "await" after "trigger" |
| E2E Timeout | Element Not Found | waitForSelector |
| Low coverage | No tests written | Mandatory coverage threshold |
▶ Example: 5. Comparison of 5 Major Testing Tools
Output:
TypeScript module compiled.
| Tool | Speed | Application |
|---|---|---|
| Vitest | ⭐⭐⭐⭐⭐ | Unit Testing |
| Vue Test Utils | ⭐⭐⭐⭐⭐ | Component Testing |
| Playwright | ⭐⭐⭐⭐ | E2E Testing |
| Cypress | ⭐⭐⭐ | E2E (Slow) |
| Testing Library | ⭐⭐⭐⭐ | Component Testing |
❓ FAQ
mount or shallowMount?shallowMount for unit tests (faster, more isolated). Use mount for integration tests (more realistic). Recommendation: Use shallowMount for component tests, and mount for a few key components.📖 Summary
- Vitest is the recommended testing framework for Vue 3 (10x faster than Jest)
- Vue Test Utils: mount / shallowMount / 5 Key APIs
- 5 Component Test Types:props / emit / slot / async / provide
- 5 Assertion Types:text / class / element / event / async
- Playwright is the recommended E2E tool (fast / cross-browser)
- Test coverage: Core 80%+, Overall 60%+
- 70% unit testing + 20% integration testing + 10% end-to-end testing
📝 Exercises
-
Basic Questions (Difficulty: ⭐)
Write 3 tests for ProductCard:
- props rendering
- emit an event
- Boundary case (inventory is 0)
-
Advanced Problems (Difficulty: ⭐⭐)
Write comprehensive tests for the
storeshopping cart:- 5 actions(addItem / removeItem / clear / load / save)
- 3 getters(itemCount / totalPrice / isEmpty)
- Pinia Test Setup
-
Challenge Problem (Difficulty: ⭐⭐⭐)
Implement a comprehensive "testing framework":
- 10 unit tests for components (3–5 tests per component)
- 5 composable tests
- 5 end-to-end tests (login / registration / search / shopping cart / checkout)
- Coverage threshold: 80%
- CI Integration (GitHub Actions)
- Cross-Browser Testing with Vitest and Playwright