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

使用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(); // 正常触发
  })
})

核心误区分析

  1. setup函数未暴露方法到组件实例
    Vue 3的setup函数中定义的变量/方法,只有通过return导出后,才会挂载到组件实例(即wrapper.vm)上。当前setup没有返回toLogin,导致wrapper.vm.toLogin不存在,vi.spyOn(wrapper.vm, 'toLogin')实际创建了一个空Spy,自然无法捕获到调用。
    但事件绑定的toLogin是直接引用setup作用域内的方法,所以实际逻辑会执行(因此setTokens被调用),只是Spy监控的对象不对。

  2. 测试初始化时机的问题
    使用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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.01 20:50:29