如何用@vue/test-utils验证子组件自定义emit触发父组件方法?
问题
我正在对一个包含<ButtonStandard>子组件的Vue组件做单元测试。这个子组件捕获点击后会触发自定义事件pressed,父组件监听该事件并执行methods.toggleArchive方法。我需要验证:当点击指定的<ButtonStandard>组件时,对应的toggleArchive方法是否被正确调用。
待测试组件代码
<script setup> /// const methods = { // 切换当前对话的归档状态 toggleArchive(){ router.visit(route('conversation.toggleArchive', props.conversation.id)); } } </script> <template> /// <ButtonStandard @pressed="methods.toggleArchive" dusk="inbox_toggle_archive" > Archive </ButtonStandard> </template>
当前测试代码
it("点击按钮应调用toggle_archive路由", () => { // 准备 const methods = { toggleArchive : vi.fn(() => console.log('success')) }; // 执行 const wrapper = mount(ConversationStatus, { propsData: { conversation : testConversation, showButtons : true } }); // 断言 expect(wrapper.find('[dusk="inbox_toggle_archive"]').exists()).toBe(true); wrapper.find('[dusk="inbox_toggle_archive"]').trigger('click'); expect(methods.toggleArchive).toHaveBeenCalled(); })
问题原因与修正方案
你的测试代码现在无法通过,核心问题是:你在测试里自己定义的methods和组件内部的methods是完全独立的两个对象,你断言的是外部的模拟函数,根本不是组件实际调用的那个方法。另外还有个细节错误:父组件监听的是子组件的pressed自定义事件,不是原生click事件,直接触发click不会触发父组件的方法。
正确的测试实现(推荐方案:验证业务逻辑)
我们可以模拟路由相关的依赖,验证router.visit是否被正确调用,这比单纯验证方法调用更贴近实际业务:
import { mount } from '@vue/test-utils'; import ConversationStatus from '@/components/ConversationStatus.vue'; import { vi } from 'vitest'; // 模拟vue-router的相关方法,避免实际路由跳转 vi.mock('vue-router', () => ({ useRouter: () => ({ visit: vi.fn() }), useRoute: () => ({ route: vi.fn((name, id) => `/conversations/${id}/toggle-archive`) }) })); it("点击归档按钮应触发正确的路由跳转", () => { // 准备测试数据 const testConversation = { id: 123 }; // 挂载组件(Vue3用props代替propsData) const wrapper = mount(ConversationStatus, { props: { conversation: testConversation, showButtons: true } }); // 获取模拟的router实例 const mockRouter = wrapper.vm.$router; // 获取按钮组件 const archiveButton = wrapper.find('[dusk="inbox_toggle_archive"]'); // 触发子组件的pressed事件(这才是父组件监听的事件) archiveButton.vm.$emit('pressed'); // 断言验证 expect(archiveButton.exists()).toBe(true); // 验证router.visit是否被调用,且参数正确 expect(mockRouter.visit).toHaveBeenCalledWith('/conversations/123/toggle-archive'); // 也可以验证route函数是否传入了正确的参数 const mockRouteFn = wrapper.vm.$route.route; expect(mockRouteFn).toHaveBeenCalledWith('conversation.toggleArchive', 123); });
另一种方案:直接验证方法调用
如果你一定要验证toggleArchive方法被调用,需要先把组件内部的方法暴露出来(因为<script setup>里的变量默认是私有的):
- 先修改组件,添加
defineExpose暴露内部方法:
<script setup> /// const methods = { toggleArchive(){ router.visit(route('conversation.toggleArchive', props.conversation.id)); } } // 暴露methods给测试用 defineExpose({ methods }); </script>
- 然后编写测试代码:
it("点击归档按钮应调用toggleArchive方法", () => { const testConversation = { id: 123 }; const wrapper = mount(ConversationStatus, { props: { conversation: testConversation, showButtons: true } }); // 给toggleArchive方法创建监听spy const toggleSpy = vi.spyOn(wrapper.vm.methods, 'toggleArchive').mockImplementation(() => {}); // 触发按钮的pressed事件 wrapper.find('[dusk="inbox_toggle_archive"]').vm.$emit('pressed'); // 断言方法被调用 expect(toggleSpy).toHaveBeenCalled(); });
核心要点总结
- 不要在测试里定义独立的
methods对象,和组件内部的完全无关 - 父组件监听的是子组件的
pressed事件,测试时要触发这个事件,不是原生click - Vue3测试里用
props代替propsData - 优先测试业务逻辑(比如路由跳转),而非内部方法调用,这样测试更稳定
内容的提问来源于stack exchange,提问作者Ben
相关产品推荐
相关产品推荐

