测试策略 #

测试类型 #

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