如何使用@vue/test-utils测试VueRouter的beforeRouteEnter钩子?
解决Vue组件beforeRouteEnter钩子的单元测试问题
我来帮你搞定这个beforeRouteEnter的测试问题!你遇到的核心问题有几个:一是直接调用钩子时没传必要的参数(to/from/next),二是组件里的store没有和测试用的store实例关联上,三是没模拟路由环境让守卫钩子正常触发。下面一步步给你解决:
1. 先搭好正确的测试环境
首先要用到@vue/test-utils的localVue创建独立的Vue实例,避免污染全局环境,同时安装Vue Router和Vuex插件:
import { shallowMount, createLocalVue } from '@vue/test-utils'; import VueRouter from 'vue-router'; import Vuex from 'vuex'; import sinon from 'sinon'; import MyComponent from '@/components/MyComponent.vue'; // 创建隔离的localVue并安装插件 const localVue = createLocalVue(); localVue.use(VueRouter); localVue.use(Vuex);
2. 正确触发beforeRouteEnter并验证action调用
beforeRouteEnter是路由守卫,它依赖路由上下文的三个参数(to/from/next),而且组件里的store最好通过实例注入(不要用全局store,否则测试时不好替换)。
先修正组件代码(推荐写法)
把组件里的全局store依赖改成通过组件实例访问,这样测试时能轻松注入测试用store:
export default { async beforeRouteEnter(to, from, next) { // 通过next回调获取组件实例,再访问$store next(async (vm) => { await vm.$store.dispatch('getFormDetails'); }); } };
对应的测试代码
通过路由跳转触发守卫钩子,同时验证action调用:
it('On route enter, it should dispatch an action to fetch form details', async () => { // 1. 创建stub化的action const getFormDetails = sinon.stub().resolves(); // 2. 创建测试用Vuex Store const store = new Vuex.Store({ actions: { getFormDetails } }); // 3. 创建测试用路由,配置组件对应的路由规则 const router = new VueRouter({ routes: [ { path: '/form', component: MyComponent } ] }); // 4. 挂载组件,注入必要的依赖 const wrapper = shallowMount(MyComponent, { localVue, store, router }); // 5. 触发路由进入操作(切换到组件对应的路由) await router.push('/form'); // 6. 断言action已被调用 expect(getFormDetails.called).to.be.true; });
如果不想修改组件(仍使用全局store),可以直接替换全局store的action:
// 假设组件里是 import store from '@/store' import store from '@/store'; it('On route enter, it should dispatch an action to fetch form details', async () => { const getFormDetails = sinon.stub().resolves(); // 替换全局store的目标action store.actions.getFormDetails = getFormDetails; const router = new VueRouter({ routes: [{ path: '/form', component: MyComponent }] }); const wrapper = shallowMount(MyComponent, { localVue, router }); await router.push('/form'); expect(getFormDetails.called).to.be.true; });
3. 测试不同路由场景(根据路径分发不同action)
如果你的组件需要根据路由路径/参数触发不同action,可以这样扩展测试:
it('Dispatch different actions based on route path', async () => { const getFormDetails = sinon.stub().resolves(); const getEditFormDetails = sinon.stub().resolves(); const store = new Vuex.Store({ actions: { getFormDetails, getEditFormDetails } }); const router = new VueRouter({ routes: [ { path: '/form', component: MyComponent }, { path: '/form/:id/edit', component: MyComponent } ] }); const wrapper = shallowMount(MyComponent, { localVue, store, router }); // 测试普通表单路径 await router.push('/form'); expect(getFormDetails.called).to.be.true; expect(getEditFormDetails.called).to.be.false; // 重置stub的调用记录 getFormDetails.resetHistory(); // 测试编辑表单路径 await router.push('/form/123/edit'); expect(getEditFormDetails.called).to.be.true; expect(getFormDetails.called).to.be.false; });
对应的组件逻辑可以写成:
export default { async beforeRouteEnter(to, from, next) { next(async (vm) => { if (to.path.includes('/edit')) { await vm.$store.dispatch('getEditFormDetails', to.params.id); } else { await vm.$store.dispatch('getFormDetails'); } }); } };
4. 手动调用beforeRouteEnter(不通过路由跳转)
如果你不想通过路由跳转触发,也可以手动调用钩子,但要传对参数并处理异步:
it('Manually trigger beforeRouteEnter and verify action', async () => { const getFormDetails = sinon.stub().resolves(); const store = new Vuex.Store({ actions: { getFormDetails } }); const wrapper = shallowMount(MyComponent, { localVue, store }); // 模拟路由对象to和from const to = { path: '/form' }; const from = { path: '/' }; // 手动调用钩子,传入next回调 await wrapper.vm.$options.beforeRouteEnter[0](to, from, (vm) => { // 钩子内部会通过这个vm参数访问组件实例的$store }); expect(getFormDetails.called).to.be.true; });
⚠️ 注意:因为beforeRouteEnter是异步钩子,测试一定要用async/await或者done回调等待异步操作完成,否则断言会在action执行前就运行,导致失败。
内容的提问来源于stack exchange,提问作者Zhave
相关产品推荐
相关产品推荐

