You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用@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>里的变量默认是私有的):

  1. 先修改组件,添加defineExpose暴露内部方法:
<script setup>
///
const methods = {
    toggleArchive(){
        router.visit(route('conversation.toggleArchive', props.conversation.id));
    }
}

// 暴露methods给测试用
defineExpose({ methods });
</script>
  1. 然后编写测试代码:
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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.29 02:48:18