测试策略 #
测试类型 #
text
Vuex 测试类型
├── 单元测试
│ ├── Mutation 测试
│ ├── Action 测试
│ └── Getter 测试
└── 集成测试
└── 组件与 Store 集成测试
测试环境配置 #
安装依赖 #
bash
npm install --save-dev jest @vue/test-utils @vue/vue3-jest
Jest 配置 #
javascript
// jest.config.js
module.exports = {
moduleFileExtensions: ['js', 'json', 'vue'],
transform: {
'^.+\\.vue$': '@vue/vue3-jest',
'^.+\\.js$': 'babel-jest'
},
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1'
},
testEnvironment: 'jsdom'
}
Mutation 测试 #
基本测试 #
javascript
// tests/unit/store/mutations.spec.js
import mutations from '@/store/modules/user/mutations'
describe('User Mutations', () => {
it('SET_PROFILE should set user profile', () => {
const state = {
profile: null,
token: null
}
const user = { id: 1, name: 'John', email: 'john@example.com' }
mutations.SET_PROFILE(state, user)
expect(state.profile).toEqual(user)
})
it('SET_TOKEN should set token', () => {
const state = {
profile: null,
token: null
}
mutations.SET_TOKEN(state, 'abc123')
expect(state.token).toBe('abc123')
})
it('CLEAR_USER should clear user data', () => {
const state = {
profile: { id: 1, name: 'John' },
token: 'abc123'
}
mutations.CLEAR_USER(state)
expect(state.profile).toBeNull()
expect(state.token).toBeNull()
})
})
批量测试 #
javascript
describe('Cart Mutations', () => {
let state
beforeEach(() => {
state = {
items: []
}
})
describe('ADD_ITEM', () => {
it('should add new item', () => {
const item = { productId: 1, quantity: 2, price: 100 }
mutations.ADD_ITEM(state, item)
expect(state.items).toHaveLength(1)
expect(state.items[0]).toEqual(item)
})
it('should increment quantity if item exists', () => {
state.items = [{ productId: 1, quantity: 2, price: 100 }]
mutations.ADD_ITEM(state, { productId: 1, quantity: 3, price: 100 })
expect(state.items).toHaveLength(1)
expect(state.items[0].quantity).toBe(5)
})
})
describe('REMOVE_ITEM', () => {
it('should remove item by productId', () => {
state.items = [
{ productId: 1, quantity: 2, price: 100 },
{ productId: 2, quantity: 1, price: 200 }
]
mutations.REMOVE_ITEM(state, 1)
expect(state.items).toHaveLength(1)
expect(state.items[0].productId).toBe(2)
})
})
})
Action 测试 #
基本测试 #
javascript
// tests/unit/store/actions.spec.js
import actions from '@/store/modules/user/actions'
import * as api from '@/api/user'
// Mock API
jest.mock('@/api/user')
describe('User Actions', () => {
let commit
let dispatch
beforeEach(() => {
commit = jest.fn()
dispatch = jest.fn()
})
afterEach(() => {
jest.clearAllMocks()
})
describe('login', () => {
it('should commit SET_PROFILE and SET_TOKEN on success', async () => {
const mockResponse = {
user: { id: 1, name: 'John' },
token: 'abc123'
}
api.login.mockResolvedValue(mockResponse)
await actions.login({ commit }, { username: 'john', password: 'secret' })
expect(commit).toHaveBeenCalledWith('SET_LOADING', true)
expect(commit).toHaveBeenCalledWith('SET_PROFILE', mockResponse.user)
expect(commit).toHaveBeenCalledWith('SET_TOKEN', mockResponse.token)
expect(commit).toHaveBeenCalledWith('SET_LOADING', false)
})
it('should commit SET_ERROR on failure', async () => {
const error = new Error('Invalid credentials')
api.login.mockRejectedValue(error)
await expect(
actions.login({ commit }, { username: 'john', password: 'wrong' })
).rejects.toThrow('Invalid credentials')
expect(commit).toHaveBeenCalledWith('SET_ERROR', 'Invalid credentials')
})
})
})
测试异步 Action #
javascript
describe('Products Actions', () => {
let commit
beforeEach(() => {
commit = jest.fn()
})
describe('fetchProducts', () => {
it('should fetch and commit products', async () => {
const mockProducts = [
{ id: 1, name: 'Product 1' },
{ id: 2, name: 'Product 2' }
]
api.fetchProducts.mockResolvedValue(mockProducts)
await actions.fetchProducts({ commit })
expect(commit).toHaveBeenCalledWith('SET_LOADING', true)
expect(commit).toHaveBeenCalledWith('SET_PRODUCTS', mockProducts)
expect(commit).toHaveBeenCalledWith('SET_LOADING', false)
})
it('should handle pagination', async () => {
const mockResponse = {
items: [{ id: 1, name: 'Product 1' }],
total: 100,
page: 1
}
api.fetchProducts.mockResolvedValue(mockResponse)
await actions.fetchProducts({ commit }, { page: 1, perPage: 10 })
expect(api.fetchProducts).toHaveBeenCalledWith({ page: 1, perPage: 10 })
})
})
})
Getter 测试 #
javascript
// tests/unit/store/getters.spec.js
import getters from '@/store/modules/cart/getters'
describe('Cart Getters', () => {
describe('itemCount', () => {
it('should return total item count', () => {
const state = {
items: [
{ productId: 1, quantity: 2 },
{ productId: 2, quantity: 3 }
]
}
const result = getters.itemCount(state)
expect(result).toBe(5)
})
it('should return 0 for empty cart', () => {
const state = { items: [] }
const result = getters.itemCount(state)
expect(result).toBe(0)
})
})
describe('total', () => {
it('should calculate total price', () => {
const state = {
items: [
{ productId: 1, quantity: 2, price: 100 },
{ productId: 2, quantity: 1, price: 200 }
]
}
const result = getters.total(state)
expect(result).toBe(400)
})
})
describe('hasDiscount', () => {
it('should return true when total > 1000', () => {
const state = {
items: [{ productId: 1, quantity: 10, price: 150 }]
}
const result = getters.hasDiscount(state, { total: 1500 })
expect(result).toBe(true)
})
})
})
组件与 Store 集成测试 #
测试组件 #
javascript
// tests/integration/components/UserProfile.spec.js
import { mount } from '@vue/test-utils'
import { createStore } from 'vuex'
import UserProfile from '@/components/UserProfile.vue'
describe('UserProfile', () => {
let store
let actions
beforeEach(() => {
actions = {
fetchProfile: jest.fn(),
updateProfile: jest.fn()
}
store = createStore({
modules: {
user: {
namespaced: true,
state: {
profile: { id: 1, name: 'John', email: 'john@example.com' },
loading: false
},
actions,
getters: {
isLoggedIn: () => true,
userName: () => 'John'
}
}
}
})
})
it('should display user name', () => {
const wrapper = mount(UserProfile, {
global: {
plugins: [store]
}
})
expect(wrapper.text()).toContain('John')
})
it('should dispatch fetchProfile on mount', () => {
mount(UserProfile, {
global: {
plugins: [store]
}
})
expect(actions.fetchProfile).toHaveBeenCalled()
})
it('should call updateProfile on form submit', async () => {
const wrapper = mount(UserProfile, {
global: {
plugins: [store]
}
})
await wrapper.find('input[name="name"]').setValue('Jane')
await wrapper.find('form').trigger('submit')
expect(actions.updateProfile).toHaveBeenCalledWith(
expect.anything(),
{ name: 'Jane' },
undefined
)
})
})
测试组合式函数 #
javascript
// tests/unit/composables/useUserStore.spec.js
import { useUserStore } from '@/composables/useUserStore'
import { createStore } from 'vuex'
describe('useUserStore', () => {
let store
beforeEach(() => {
store = createStore({
modules: {
user: {
namespaced: true,
state: {
profile: { id: 1, name: 'John' },
token: 'abc123'
},
getters: {
isLoggedIn: state => !!state.token,
userName: state => state.profile?.name || 'Guest'
},
actions: {
login: jest.fn(),
logout: jest.fn()
}
}
}
})
})
it('should return computed state', () => {
const { profile, isLoggedIn, userName } = useUserStore()
expect(profile.value).toEqual({ id: 1, name: 'John' })
expect(isLoggedIn.value).toBe(true)
expect(userName.value).toBe('John')
})
})
测试覆盖率 #
配置覆盖率 #
javascript
// jest.config.js
module.exports = {
// ...
collectCoverage: true,
collectCoverageFrom: [
'src/store/**/*.{js,ts}',
'!src/store/index.js'
],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
}
}
}
最佳实践 #
1. 隔离测试 #
javascript
// 推荐:每个测试独立
beforeEach(() => {
state = { ...initialState }
})
// 不推荐:共享状态
let state = { ...initialState }
2. Mock 外部依赖 #
javascript
// Mock API
jest.mock('@/api/user')
// Mock router
jest.mock('vue-router', () => ({
useRouter: () => ({
push: jest.fn()
})
}))
3. 测试边界情况 #
javascript
describe('itemCount', () => {
it('should return 0 for empty cart', () => {
const state = { items: [] }
expect(getters.itemCount(state)).toBe(0)
})
it('should handle large quantities', () => {
const state = { items: [{ quantity: 1000000 }] }
expect(getters.itemCount(state)).toBe(1000000)
})
})
总结 #
测试策略要点:
| 测试类型 | 重点 |
|---|---|
| Mutation | 测试状态变更 |
| Action | 测试异步操作和 commit |
| Getter | 测试计算结果 |
| 集成测试 | 测试组件与 Store 交互 |
继续学习 性能优化,了解 Vuex 性能优化技巧。
最后更新:2026-03-28