news 2026/9/19 22:34:15

Vuex 4 单元测试实战指南:Mutation、Action、Getter 的测试方法与运行方式

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Vuex 4 单元测试实战指南:Mutation、Action、Getter 的测试方法与运行方式

Vuex 4 单元测试实战指南:Mutation、Action、Getter 的测试方法与运行方式

【免费下载链接】vuex🗃️ Centralized State Management for Vue.js.项目地址: https://gitcode.com/gh_mirrors/vu/vuex

本文以 Vuex 官方文档日文版 测试指南 为骨架,结合仓库源码(src/store.jssrc/store-util.js)与真实示例(examples/classic/shopping-cart/)展开。核心目标是:以最轻量、最直接的方式验证 Vuex 状态管理逻辑的正确性——只测你关心的纯函数,不依赖浏览器、不启动完整应用。读完本文,你将掌握 mutation / action / getter 三种单元测试的完整写法、inject-loader依赖注入技巧、testAction与 Sinon spy 两种断言策略,以及 Node / 浏览器 / Karma 三种测试运行环境。

Vuex 4 是面向 Vue 3 的集中式状态管理方案(当前仓库版本4.1.0,见 package.json)。在 Vuex 中,状态变更的全部逻辑都收敛在 mutation、action、getter 三个纯函数/半纯函数里,因此单元测试的关注点非常集中:mutation 与 action 是核心测试对象,getter 在计算逻辑复杂时也值得测试(见 docs/ja/guide/testing.md)。


一、为什么 Vuex 这么好测:先理解三个函数的本质

从源码结构看,Vuex 的测试友好性来自其设计本身。打开 src/store-util.js,可以看到三个核心函数的注册包装:

  • mutationregisterMutation把 handler 包装为handler.call(store, local.state, payload)——即 mutation 只是(state, payload) => void的纯函数,完全由参数驱动(src/store-util.js#L222-L227);
  • actionregisterAction把 handler 包装为接收{ dispatch, commit, getters, state, rootGetters, rootState }上下文对象,返回值会被Promise.resolve统一包裹(src/store-util.js#L229-L252);
  • getterregisterGetter把 getter 包装为接收(localState, localGetters, rootState, rootGetters)的函数(src/store-util.js#L254-L269)。

结论很清晰:mutation 和 getter 是纯函数,直接传参调用即可测试;action 只是多了一个可注入的上下文参数,mock 掉commit/ 外部 API 即可测试。这正是本文所有测试写法的底层依据。


二、测试 Mutation:把store.js中的 mutation 作为命名导出

mutation 是完全依赖参数的函数,测试最简单。一个实用技巧是:如果你的 mutation 写在store.js中,除了默认导出 store,还要把 mutation 作为命名导出

const state = { ... } // 命名导出 mutations,便于测试直接引用 export const mutations = { ... } export default createStore({ state, mutations })

注意:这里是 Vuex 4 的createStoreAPI(见 src/store.js#L15-L17);Vuex 3.x 中对应的是new Vuex.Store({ ... }),命名导出的思路完全一致。

然后用 Mocha + Chai 测试(可换成任意测试框架/断言库,如 Jest):

// mutations.js export const mutations = { increment: state => state.count++ }
// mutations.spec.js import { expect } from 'chai' import { mutations } from './store' // 解构出要测的 mutation const { increment } = mutations describe('mutations', () => { it('INCREMENT', () => { // 用 mock 的 state 作为入参 const state = { count: 0 } // 直接调用 mutation increment(state) // 断言结果 expect(state.count).to.equal(1) }) })

测试要点

  • 不创建真实 Store、不挂载组件、不需要 Vue 实例,只构造一个普通对象作为state传入;
  • mutation 内部对state的修改是引用型的,断言 mock state 即可;
  • 该写法与仓库单元测试中直接构造new Vuex.Store({ mutations: { inc: ... } })再调用的方式互为补充(见 test/unit/modules.spec.js#L10-L19)。

三、测试 Action:mock 依赖与 mock commit

action 可能调用外部 API,因此测试时通常需要分层 mock:把 API 调用抽象成 service(例如api/shop.js),测试时 mock 掉该 service。为了便捷注入 mock 依赖,官方推荐使用webpack +inject-loader打包测试文件。

3.1 待测的异步 action

// actions.js import shop from '../api/shop' export const getAllProducts = ({ commit }) => { commit('REQUEST_PRODUCTS') shop.getProducts(products => { commit('RECEIVE_PRODUCTS', products) }) }

3.2 使用 inject-loader 注入 mock 依赖

// actions.spec.js // inline loader 需要 require 语法 // inject-loader 会返回一个模块工厂(module factory), // 允许我们注入 mock 化的依赖 import { expect } from 'chai' const actionsInjector = require('inject-loader!./actions') // 用 mock 创建模块 const actions = actionsInjector({ '../api/shop': { getProducts (cb) { setTimeout(() => { cb([ /* mocked response */ ]) }, 100) } } })

这里require('inject-loader!./actions')是 webpack 的 inline loader 语法,inject-loader会把actions.jsimport shop from '../api/shop'的依赖替换为传入的 mock 对象,从而在不改动源码的前提下隔离外部调用

佐证:仓库中的购物车示例正是把 API 封装在独立的 service 层——examples/classic/shopping-cart/api/shop.js 用async getProducts()/async buyProducts(products)模拟了 100ms 延迟与随机失败,而 store 模块只关心commit(见 examples/classic/shopping-cart/store/modules/cart.js#L33-L48)。这种“service 与 store 分离”的架构正是可测试性的来源。

3.3 testAction 辅助函数:按顺序断言 commit 调用

// 辅助函数:验证 action 是否按预期顺序、以预期载荷调用 mutation const testAction = (action, payload, state, expectedMutations, done) => { let count = 0 // mock commit const commit = (type, payload) => { const mutation = expectedMutations[count] try { expect(type).to.equal(mutation.type) expect(payload).to.deep.equal(mutation.payload) } catch (error) { done(error) } count++ if (count >= expectedMutations.length) { done() } } // 用 mock 的 store 上下文和参数调用 action action({ commit, state }, payload) // 检查是否有多余的 mutation 被调用 if (expectedMutations.length === 0) { expect(count).to.equal(0) done() } } describe('actions', () => { it('getAllProducts', done => { testAction(actions.getAllProducts, null, {}, [ { type: 'REQUEST_PRODUCTS' }, { type: 'RECEIVE_PRODUCTS', payload: { /* mocked response */ } } ], done) }) })

testAction 设计要点

  • 它用expectedMutations数组顺序断言 action 发起的每次commit的类型与载荷;
  • 采用回调式异步测试(done):当所有预期 mutation 都被调用后调用done()结束测试;
  • expectedMutations.length === 0时,断言 action 没有发起任何 commit——用于测试不触发 mutation 的 action;
  • 因为commit是 mock 的,测试天然不依赖真实 Store。

3.4 用 Sinon spy 替代 testAction

如果测试环境有 spy 工具(如 Sinon.JS),可以更简洁——收集commit的所有调用参数并整体断言

describe('actions', () => { it('getAllProducts', () => { const commit = sinon.spy() const state = {} actions.getAllProducts({ commit, state }) expect(commit.args).to.deep.equal([ ['REQUEST_PRODUCTS'], ['RECEIVE_PRODUCTS', { /* mocked response */ }] ]) }) })

sinon.spy()会记录每次调用的参数到commit.args,一次性断言调用序列,比手写testAction更少样板代码。若断言库支持,也可用 Jest 的jest.fn()+toHaveBeenNthCalledWith达到同样效果(仓库测试中即有jest.fn()spy 用法,见 test/unit/modules.spec.js#L66-L79)。


四、测试 Getter:复杂计算值得单测

如果 getter 包含复杂计算,就值得编写测试。getter 与 mutation 同理,作为纯函数测试起来非常直接。

// getters.js export const getters = { filteredProducts (state, { filterCategory }) { return state.products.filter(product => { return product.category === filterCategory }) } }
// getters.spec.js import { expect } from 'chai' import { getters } from './getters' describe('getters', () => { it('filteredProducts', () => { // mock state const state = { products: [ { id: 1, title: 'Apple', category: 'fruit' }, { id: 2, title: 'Orange', category: 'fruit' }, { id: 3, title: 'Carrot', category: 'vegetable' } ] } // 模拟第二个参数(getters 上下文) const filterCategory = 'fruit' // 直接调用 getter 获取结果 const result = getters.filteredProducts(state, { filterCategory }) // 断言结果 expect(result).to.deep.equal([ { id: 1, title: 'Apple', category: 'fruit' }, { id: 2, title: 'Orange', category: 'fruit' } ]) }) })

注意 getter 的签名:根据源码 src/store-util.js#L261-L268,getter 实际接收(localState, localGetters, rootState, rootGetters)四个参数。本文示例只用到前两个(state{ filterCategory }解构);当 getter 依赖其他 getter 或根状态时,例如购物车示例中的cartProducts: (state, getters, rootState) => ...(见 examples/classic/shopping-cart/store/modules/cart.js#L13-L23),测试时需一并 mock 传入。


五、测试的运行方式

只要 mutation 和 action 写得规范,mock 之后测试代码通常不直接依赖浏览器 API,因此可以把测试用 webpack 打包后直接在 Node 中运行;如需真实浏览器环境,则用mocha-loader或 Karma +karma-webpack

5.1 在 Node 中运行

创建如下 webpack 配置(配合相应的.babelrc):

// webpack.config.js module.exports = { entry: './test.js', output: { path: __dirname, filename: 'test-bundle.js' }, module: { loaders: [ { test: /\.js$/, loader: 'babel-loader', exclude: /node_modules/ } ] } }

然后执行:

webpack mocha test-bundle.js

要点babel-loader负责把 ES2015 模块语法(本文示例中的import/export)转译为 CommonJS,inject-loader!这类 inline loader 依赖 webpack 解析,所以必须经 webpack 打包后再交给 Mocha 运行。

延伸:当前仓库自身的单元测试则采用 Jest + jsdom 的方案——jest.config.js 中testEnvironment: 'jsdom'testMatch匹配test/**/*.spec.js,通过moduleNameMapper@/映射到src/。如果你想在 Vuex 4 仓库内部跑测试,可执行npm run test:unit(见 package.json 的 scripts)。这与本文面向“应用开发者测试自己 store”的场景互补。

5.2 在浏览器中运行(mocha-loader)

  1. 安装mocha-loader
  2. 把上面 webpack 配置的entry改为'mocha-loader!babel-loader!./test.js'
  3. 使用该配置启动webpack-dev-server
  4. 在浏览器中打开localhost:8080/webpack-dev-server/test-bundle查看测试结果。

mocha-loader会在浏览器中渲染 Mocha 的测试报告界面,适合需要验证真实浏览器行为(如涉及 DOM 或浏览器 API)的场景。

5.3 在浏览器中运行(Karma + karma-webpack)

如果需要跨浏览器(Chrome / Firefox / IE 等)跑测试并接入 CI,推荐 Karma +karma-webpack。具体配置方法可参考 vue-loader 文档 中的测试环境搭建说明(该文档同样适用于纯 Vuex 测试)。Karma 的优势在于可以启动多个真实浏览器执行同一份测试包,并生成覆盖率报告。


六、与 Vue 组件测试的衔接

本文测试的对象是 store 层的纯逻辑,而仓库中还提供了mapState/mapMutations/mapGetters/mapActions等映射辅助函数(见 src/helpers.js),它们把 store 能力绑定到组件。对组件层的测试(例如验证mapGetters正确注入 computed)通常需要真实的 Store + 组件挂载,仓库的 test/unit/helpers.spec.js 演示了这种写法:

const store = new Vuex.Store({ state: { a: 1 }, getters: { b: () => 2 } }) const vm = mount(store, { computed: mapState({ a: (state, getters) => state.a + getters.b }) }) expect(vm.a).toBe(3)

建议的分层策略:store 纯逻辑用本文的轻量单测(快、稳),组件与 store 的接线逻辑用带挂载的集成测试。两者互补,共同覆盖 Vuex 应用的核心行为。


结语

Vuex 的单元测试核心思想可以概括为一句话:把状态逻辑收敛为纯函数,然后用 mock 隔离一切外部依赖。mutation 与 getter 直接传参调用,action 通过inject-loader注入 mock service、用testAction或 Sinon spy 断言 commit 序列,最后用 webpack + Mocha(Node)、mocha-loader(浏览器)或 Karma(多浏览器)运行。这套方法论不依赖具体测试框架,可平滑迁移到 Jest、Vitest 等现代工具链,是保障 Vuex 应用长期可维护性的基础工程能力。

【免费下载链接】vuex🗃️ Centralized State Management for Vue.js.项目地址: https://gitcode.com/gh_mirrors/vu/vuex

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/19 22:34:05

BrewUI体验:为Homebrew包管理打造的可视化仪表盘

说实话,最早看到BrewUI这个项目的时候,我内心是有点不屑的。Homebrew这套包管理工具,从入行第一天就是跟终端打交道的,brew install、brew upgrade、brew list这些命令早就在我肌肉记忆里了,一个命令能解决的事为什么要…

作者头像 李华
网站建设 2026/9/19 22:28:26

CPU大核闲置?从调度原理到强制绑核的完整实操指南

先说一个很多人的困惑:明明换了新平台、买了一大堆高性能核心,结果某个程序还是卡得不行。打开任务管理器一看,CPU总占用率不高,大核有一多半是闲着的,反而是低功耗小核心上跑满了线程。这类问题在Intel 12代/13代/14代…

作者头像 李华
网站建设 2026/9/19 22:28:22

微信小程序音乐播放器开发:从播放内核到歌词同步完整实践

简介:《音乐播放器微信小程序的设计与实现》是一份面向微信小程序初学者的完整设计方案,也适合计算机专业学生作为课程设计或毕业设计参考。内容先做需求分析,覆盖播放控制、播放列表、音乐推荐、搜索、歌词显示、分享等核心功能,…

作者头像 李华