使用Vitest与Vue-Test-Utils测试子组件事件触发父函数失败问题
Vue组件测试:事件触发后方法Spy未捕获的误区解析
问题场景
需要验证子组件触发的onLogin事件是否能正确调用父组件Login.vue中的toLogin函数,但测试结果显示只有Pinia Store的setTokens被调用,toLogin的Spy未捕获到调用,报错如下:
AssertionError: expected "toLogin" to be called once
❯ src/components/tests:136:24
- Expected "1"
- Received "0"
相关代码
Login.vue
<template> <ChildComponent ref="child" @onLogin="toLogin" /> </template> <script> import { useAuthStore } from "@/stores/AuthStore.js"; //import Pinia Store import { userLogin } from "@/service/authService.js"; // import axios functions from another js file import ChildComponent from "@/components/ChildComponent.vue"; export default { name: "Login", components: { ChildComponent, }, setup() { const AuthStore = useAuthStore(); const toLogin = async (param) => { try { const res = await userLogin (param); AuthStore.setTokens(res); } catch (error) { console.log(error); } }; } </script>
login.spec.js
import { describe, it, expect, vi, beforeAll } from 'vitest'; import { shallowMount, flushPromises } from '@vue/test-utils'; import { createTestingPinia } from "@pinia/testing"; import Login from "@/views/user/Login.vue" import { useAuthStore } from "@/stores/AuthStore.js"; describe('Login', () => { let wrapper = null; beforeAll(() => { wrapper = shallowMount(Login, { global: { plugins: [createTestingPinia({ createSpy: vi.fn })], }, }); }) it('login by emitted events', async () => { const AuthStore = useAuthStore(); const loginParam = { email: 'dummy@email.com', password: '12345', }; const spyOnLogin = vi.spyOn(wrapper.vm, 'toLogin'); const spyOnStore = vi.spyOn(AuthStore, 'setTokens'); await wrapper.vm.$refs.child.$emit('onLogin', loginParam); await wrapper.vm.$nextTick(); await flushPromises(); expect(spyOnLogin).toHaveBeenCalledOnce(); // 未触发 expect(spyOnStore).toHaveBeenCalledOnce(); // 正常触发 }) })
核心误区分析
setup函数未暴露方法到组件实例
Vue 3的setup函数中定义的变量/方法,只有通过return导出后,才会挂载到组件实例(即wrapper.vm)上。当前setup没有返回toLogin,导致wrapper.vm.toLogin不存在,vi.spyOn(wrapper.vm, 'toLogin')实际创建了一个空Spy,自然无法捕获到调用。
但事件绑定的toLogin是直接引用setup作用域内的方法,所以实际逻辑会执行(因此setTokens被调用),只是Spy监控的对象不对。测试初始化时机的问题
使用beforeAll初始化wrapper会导致所有测试用例共享同一个组件实例,容易造成状态污染,应该改用beforeEach确保每个测试用例有独立的组件实例。
修复方案
方案1:暴露toLogin到组件实例
修改Login.vue的setup函数,返回toLogin:
setup() { const AuthStore = useAuthStore(); const toLogin = async (param) => { try { const res = await userLogin (param); AuthStore.setTokens(res); } catch (error) { console.log(error); } }; return { toLogin }; // 导出方法到组件实例 }
方案2:调整测试逻辑,直接验证事件绑定效果
如果不需要监控toLogin本身,而是验证事件触发后是否执行了对应的业务逻辑,可以mockuserLogin并验证其调用:
import { userLogin } from "@/service/authService.js"; vi.mock("@/service/authService.js", () => ({ userLogin: vi.fn().mockResolvedValue({ token: 'dummy-token' }) })); it('login by emitted events', async () => { const AuthStore = useAuthStore(); const loginParam = { email: 'dummy@email.com', password: '12345', }; const spyOnUserLogin = vi.spyOn(userLogin); const spyOnStore = vi.spyOn(AuthStore, 'setTokens'); // 触发子组件事件 await wrapper.findComponent(ChildComponent).vm.$emit('onLogin', loginParam); await flushPromises(); expect(spyOnUserLogin).toHaveBeenCalledWith(loginParam); expect(spyOnStore).toHaveBeenCalledOnce(); })
方案3:修改测试初始化时机
将beforeAll改为beforeEach,确保测试隔离:
describe('Login', () => { let wrapper = null; beforeEach(() => { wrapper = shallowMount(Login, { global: { plugins: [createTestingPinia({ createSpy: vi.fn })], }, }); }) // ...测试用例 })
内容的提问来源于stack exchange,提问作者Joshua Yu
相关产品推荐
相关产品推荐

