Jest Vue 测试 #
Vue 测试概述 #
Vue 组件测试使用 Vue Test Utils 作为官方测试工具,支持 Vue 2 和 Vue 3。
text
┌─────────────────────────────────────────────────────────────┐
│ Vue 测试内容 │
├─────────────────────────────────────────────────────────────┤
│ 1. 组件渲染 - 验证组件正确渲染 │
│ 2. Props 测试 - 测试组件属性 │
│ 3. 事件测试 - 测试组件事件 │
│ 4. 插槽测试 - 测试插槽内容 │
│ 5. Vuex 测试 - 测试状态管理 │
│ 6. Router 测试 - 测试路由 │
└─────────────────────────────────────────────────────────────┘
环境配置 #
安装依赖 #
bash
# Vue 3
npm install --save-dev @vue/test-utils@next vue-jest@next @vue/vue3-jest
# Vue 2
npm install --save-dev @vue/test-utils vue-jest
配置 Jest #
javascript
// jest.config.js
module.exports = {
testEnvironment: 'jsdom',
transform: {
'^.+\\.vue$': '@vue/vue3-jest',
'^.+\\.js$': 'babel-jest',
},
moduleFileExtensions: ['vue', 'js', 'json'],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
},
};
基本组件测试 #
简单组件 #
vue
<!-- Greeting.vue -->
<template>
<h1>Hello, {{ name }}!</h1>
</template>
<script>
export default {
props: {
name: {
type: String,
required: true,
},
},
};
</script>
javascript
// Greeting.test.js
import { mount } from '@vue/test-utils';
import Greeting from './Greeting.vue';
test('renders greeting', () => {
const wrapper = mount(Greeting, {
props: {
name: 'John',
},
});
expect(wrapper.text()).toContain('Hello, John!');
});
带状态的组件 #
vue
<!-- Counter.vue -->
<template>
<div>
<span data-testid="count">{{ count }}</span>
<button @click="increment">Increment</button>
<button @click="decrement">Decrement</button>
</div>
</template>
<script>
export default {
data() {
return {
count: 0,
};
},
methods: {
increment() {
this.count++;
},
decrement() {
this.count--;
},
},
};
</script>
javascript
// Counter.test.js
import { mount } from '@vue/test-utils';
import Counter from './Counter.vue';
test('counter functionality', async () => {
const wrapper = mount(Counter);
expect(wrapper.find('[data-testid="count"]').text()).toBe('0');
await wrapper.find('button').trigger('click');
expect(wrapper.find('[data-testid="count"]').text()).toBe('1');
await wrapper.findAll('button')[1].trigger('click');
expect(wrapper.find('[data-testid="count"]').text()).toBe('0');
});
Props 测试 #
vue
<!-- Button.vue -->
<template>
<button
:class="['btn', `btn-${variant}`]"
:disabled="disabled"
@click="$emit('click')"
>
<slot />
</button>
</template>
<script>
export default {
props: {
variant: {
type: String,
default: 'primary',
},
disabled: {
type: Boolean,
default: false,
},
},
};
</script>
javascript
// Button.test.js
import { mount } from '@vue/test-utils';
import Button from './Button.vue';
describe('Button', () => {
test('renders with default props', () => {
const wrapper = mount(Button, {
slots: {
default: 'Click me',
},
});
expect(wrapper.text()).toContain('Click me');
expect(wrapper.classes()).toContain('btn-primary');
});
test('renders with variant', () => {
const wrapper = mount(Button, {
props: {
variant: 'secondary',
},
});
expect(wrapper.classes()).toContain('btn-secondary');
});
test('renders disabled', () => {
const wrapper = mount(Button, {
props: {
disabled: true,
},
});
expect(wrapper.attributes('disabled')).toBe('');
});
test('emits click event', async () => {
const wrapper = mount(Button);
await wrapper.trigger('click');
expect(wrapper.emitted()).toHaveProperty('click');
});
});
事件测试 #
vue
<!-- SearchInput.vue -->
<template>
<input
type="text"
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
@keyup.enter="$emit('search', modelValue)"
/>
</template>
<script>
export default {
props: ['modelValue'],
emits: ['update:modelValue', 'search'],
};
</script>
javascript
// SearchInput.test.js
import { mount } from '@vue/test-utils';
import SearchInput from './SearchInput.vue';
test('emits update:modelValue on input', async () => {
const wrapper = mount(SearchInput, {
props: {
modelValue: '',
},
});
await wrapper.find('input').setValue('test');
expect(wrapper.emitted('update:modelValue')[0]).toEqual(['test']);
});
test('emits search on enter', async () => {
const wrapper = mount(SearchInput, {
props: {
modelValue: 'search term',
},
});
await wrapper.find('input').trigger('keyup.enter');
expect(wrapper.emitted('search')[0]).toEqual(['search term']);
});
插槽测试 #
vue
<!-- Card.vue -->
<template>
<div class="card">
<div class="card-header">
<slot name="header" />
</div>
<div class="card-body">
<slot />
</div>
<div class="card-footer">
<slot name="footer" />
</div>
</div>
</template>
javascript
// Card.test.js
import { mount } from '@vue/test-utils';
import Card from './Card.vue';
test('renders slots', () => {
const wrapper = mount(Card, {
slots: {
header: '<h1>Header</h1>',
default: '<p>Content</p>',
footer: '<span>Footer</span>',
},
});
expect(wrapper.find('.card-header').html()).toContain('Header');
expect(wrapper.find('.card-body').html()).toContain('Content');
expect(wrapper.find('.card-footer').html()).toContain('Footer');
});
test('renders scoped slots', () => {
const wrapper = mount(Card, {
slots: {
default: {
template: '<p>{{ props.data }}</p>',
data: 'test data',
},
},
});
expect(wrapper.text()).toContain('test data');
});
Composition API 测试 #
vue
<!-- useCounter.vue -->
<template>
<div>
<span>{{ count }}</span>
<button @click="increment">+</button>
<button @click="decrement">-</button>
</div>
</template>
<script setup>
import { ref } from 'vue';
const count = ref(0);
function increment() {
count.value++;
}
function decrement() {
count.value--;
}
defineExpose({ count, increment, decrement });
</script>
javascript
// useCounter.test.js
import { mount } from '@vue/test-utils';
import Counter from './useCounter.vue';
test('composition API counter', async () => {
const wrapper = mount(Counter);
expect(wrapper.text()).toContain('0');
await wrapper.findAll('button')[0].trigger('click');
expect(wrapper.text()).toContain('1');
await wrapper.findAll('button')[1].trigger('click');
expect(wrapper.text()).toContain('0');
});
Vuex 测试 #
javascript
// store.js
import { createStore } from 'vuex';
export default createStore({
state: {
count: 0,
},
mutations: {
increment(state) {
state.count++;
},
},
actions: {
increment({ commit }) {
commit('increment');
},
},
});
vue
<!-- Counter.vue -->
<template>
<div>
<span>{{ count }}</span>
<button @click="increment">Increment</button>
</div>
</template>
<script>
import { mapState, mapActions } from 'vuex';
export default {
computed: {
...mapState(['count']),
},
methods: {
...mapActions(['increment']),
},
};
</script>
javascript
// Counter.test.js
import { mount } from '@vue/test-utils';
import { createStore } from 'vuex';
import Counter from './Counter.vue';
test('counter with Vuex', async () => {
const store = createStore({
state: { count: 0 },
mutations: {
increment(state) {
state.count++;
},
},
});
const wrapper = mount(Counter, {
global: {
plugins: [store],
},
});
expect(wrapper.text()).toContain('0');
await wrapper.find('button').trigger('click');
expect(wrapper.text()).toContain('1');
});
Pinia 测试 #
javascript
// stores/counter.js
import { defineStore } from 'pinia';
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
}),
actions: {
increment() {
this.count++;
},
},
});
javascript
// Counter.test.js
import { mount } from '@vue/test-utils';
import { createPinia, setActivePinia } from 'pinia';
import Counter from './Counter.vue';
beforeEach(() => {
setActivePinia(createPinia());
});
test('counter with Pinia', async () => {
const wrapper = mount(Counter);
const store = useCounterStore();
expect(wrapper.text()).toContain('0');
await wrapper.find('button').trigger('click');
expect(store.count).toBe(1);
});
Vue Router 测试 #
javascript
// router.js
import { createRouter, createWebHistory } from 'vue-router';
const routes = [
{ path: '/', component: Home },
{ path: '/about', component: About },
];
export default createRouter({
history: createWebHistory(),
routes,
});
javascript
// Navigation.test.js
import { mount } from '@vue/test-utils';
import { createRouter, createWebHistory } from 'vue-router';
import Navigation from './Navigation.vue';
test('navigation links', async () => {
const router = createRouter({
history: createWebHistory(),
routes: [
{ path: '/', component: { template: '<div>Home</div>' } },
{ path: '/about', component: { template: '<div>About</div>' } },
],
});
const wrapper = mount(Navigation, {
global: {
plugins: [router],
},
});
expect(wrapper.find('a[href="/"]').exists()).toBe(true);
expect(wrapper.find('a[href="/about"]').exists()).toBe(true);
});
异步组件测试 #
vue
<!-- UserList.vue -->
<template>
<div v-if="loading">Loading...</div>
<div v-else-if="error">Error: {{ error }}</div>
<ul v-else>
<li v-for="user in users" :key="user.id">{{ user.name }}</li>
</ul>
</template>
<script>
export default {
data() {
return {
users: [],
loading: true,
error: null,
};
},
async mounted() {
try {
const response = await fetch('/api/users');
this.users = await response.json();
} catch (error) {
this.error = error.message;
} finally {
this.loading = false;
}
},
};
</script>
javascript
// UserList.test.js
import { mount, flushPromises } from '@vue/test-utils';
import UserList from './UserList.vue';
beforeEach(() => {
global.fetch = jest.fn();
});
test('displays user list', async () => {
fetch.mockResolvedValueOnce({
json: () => Promise.resolve([
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' },
]),
});
const wrapper = mount(UserList);
expect(wrapper.text()).toContain('Loading...');
await flushPromises();
expect(wrapper.text()).toContain('John');
expect(wrapper.text()).toContain('Jane');
});
最佳实践 #
1. 使用 data-testid #
vue
<template>
<button data-testid="submit-button">Submit</button>
</template>
javascript
wrapper.find('[data-testid="submit-button"]');
2. 测试用户行为 #
javascript
// ✅ 好的做法
await wrapper.find('input').setValue('test');
await wrapper.find('form').trigger('submit');
// ❌ 不好的做法
wrapper.vm.inputValue = 'test';
wrapper.vm.submit();
3. 使用全局组件 #
javascript
const wrapper = mount(Component, {
global: {
components: {
MyComponent,
},
},
});
下一步 #
现在你已经掌握了 Vue 测试,接下来学习 Node.js 测试 实战后端测试!
最后更新:2026-03-28