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

Vue3组件单元测试中如何Mock自定义Composable

Vue3 Composition API中正确Mock自定义Composable的方法

问题背景

需要为基于VueDatePicker实现的日期时间选择栏组件编写单元测试:点击“Parādīt”按钮时,若用户已选择时间则向父组件发射选中日期,否则无操作。已将时间、日期变量及判断时间是否选中的isDateSet函数抽离为useDate自定义Composable,且完成了该Composable的单元测试,但无法按需Mock该Composable的isDateSet函数使其返回true。

现有测试代码

// 所有必要导入已在上方
const wrapper = shallowMount(TheTimePickerBar);
describe("The Time Picker Bar", () => {
    it("Component emit date if isDateSet() returns true", () => {    
        wrapper.vm.isDateSet = vi.fn().mockReturnValue(true); // 尝试Mock组件中使用的composable函数,但emitSelectedDate中仍返回false
        wrapper.vm.emitSelectedDate(); // 用户点击按钮时触发的方法
        expect(wrapper.emitted('selected-date')).toBeTruthy(); // 断言失败
    });
});

测试报错信息

tests/TheTimePickerBar.spec.ts > The Time Picker Bar > component emit date if isDateSet() returns true
AssertionError: expected undefined to be true // Object.is equality

- Expected:
true

+ Received:
undefined

 ❯ tests/TheTimePickerBar.spec.ts:35:50

相关代码

useDate Composable代码

import { Ref, ref } from "vue";

interface Time {
  hours: number;
  minutes: number;
  seconds: number;
}

export function useDate() {
  const date = ref(new Date());
  const time: Ref<Time> = ref({ hours: 0, minutes: 0, seconds: 0 });
  const isValidDate: Ref<undefined | boolean> = ref(undefined);
  const isValidTime: Ref<undefined | boolean> = ref(undefined);

  const isDateSet = (): boolean => {
    if (!time.value.hours) {
      isValidTime.value = false;

      return false;
    }

    isValidTime.value = undefined;
    isValidDate.value = undefined;

    date.value.setHours(
      time.value.hours,
      time.value.minutes,
      time.value.seconds
    );

    return true;
  };

  return { date, time, isValidDate, isValidTime, isDateSet };
}

组件中使用方式

<template>
<!-- 此处省略bootstrap vue和VueDatePicker组件代码,它们通过v-model绑定日期时间值 -->
    <b-button
     @click="emitSelectedDate"
     variant="primary"
     class="btn-max-width"
    >
        Parādīt
    </b-button>
</template>
  
<script setup lang="ts">

import { useDate }from '@src/composables/useDate.ts'

import { constants } from '@src/constants/constants';

import VueDatePicker from '@vuepic/vue-datepicker';

const emit = defineEmits<{
    selectedDate: [date: string]
}>();
// 解构composable,除isDateSet为函数外其余均为响应式变量
const { date, time, isValidDate, isValidTime, isDateSet } = useDate(); 

// 当isDateSet()返回true时,向父组件发射date变量
const emitSelectedDate = ((): void => {
    console.log('component isDateSet()',isDateSet()); // 无论怎么Mock,这里始终返回false
    if(isDateSet())
      emit('selectedDate',  date.value.toString());
});
</script>

已尝试方案

  • 使用vi.fn()直接Mock函数返回值,但emitSelectedDate中仍获取到false;
  • 使用vi.mock()Mock整个Composable,但出现TypeError错误。

技术栈:Vue3(Composition API写法)、Vue Test Utils、Vitest(替换Jest后问题依旧)


解决方案

在Vue3的<script setup>中,解构出来的isDateSet函数是组件内部的闭包引用,直接修改wrapper.vm.isDateSet无法覆盖组件内部实际调用的函数。正确的做法是Mock整个useDate Composable的导出,确保组件使用的是Mock后的版本。

正确的测试代码示例

import { shallowMount } from '@vue/test-utils';
import { vi } from 'vitest';
import TheTimePickerBar from '@src/components/TheTimePickerBar.vue';

// 提前MockuseDate模块
vi.mock('@src/composables/useDate.ts', () => ({
  useDate: vi.fn(() => ({
    date: new Date(),
    time: { hours: 0, minutes: 0, seconds: 0 },
    isValidDate: undefined,
    isValidTime: undefined,
    // 设置默认Mock实现
    isDateSet: vi.fn().mockReturnValue(false)
  }))
}));

// 从Mock模块导入useDate,获取Mock引用
import { useDate } from '@src/composables/useDate.ts';

describe("The Time Picker Bar", () => {
  it("Component emits date when isDateSet() returns true", () => {    
    // 自定义Mock返回值,修改isDateSet的实现
    const mockIsDateSet = vi.fn().mockReturnValue(true);
    (useDate as vi.Mock).mockReturnValue({
      date: new Date(),
      time: { hours: 1, minutes: 0, seconds: 0 },
      isValidDate: undefined,
      isValidTime: undefined,
      isDateSet: mockIsDateSet
    });

    // 重新挂载组件,确保使用最新Mock
    const wrapper = shallowMount(TheTimePickerBar);
    
    wrapper.vm.emitSelectedDate();
    expect(wrapper.emitted('selected-date')).toBeTruthy();
    // 额外断言isDateSet被调用
    expect(mockIsDateSet).toHaveBeenCalled();
  });

  it("Component does NOT emit date when isDateSet() returns false", () => {    
    (useDate as vi.Mock).mockReturnValue({
      date: new Date(),
      time: { hours: 0, minutes: 0, seconds: 0 },
      isValidDate: undefined,
      isValidTime: undefined,
      isDateSet: vi.fn().mockReturnValue(false)
    });

    const wrapper = shallowMount(TheTimePickerBar);
    
    wrapper.vm.emitSelectedDate();
    expect(wrapper.emitted('selected-date')).toBeFalsy();
  });
});

关键说明

  1. 提前Mock模块:在测试文件顶部用vi.mock()Mock整个useDate模块,确保组件导入的是Mock版本而非真实实现。
  2. 获取Mock引用:从Mock模块导入useDate,这样可以在测试用例中动态修改其返回值。
  3. 动态修改Mock实现:每个测试用例中,重新设置useDate的Mock返回值,按需控制isDateSet的结果,然后重新挂载组件(因为<script setup>代码在组件挂载时执行,必须在挂载前修改Mock)。
  4. 匹配原Composable结构:Mock返回的对象必须包含原Composable导出的所有属性,且类型匹配,避免出现TypeError。

内容的提问来源于stack exchange,提问作者Inex

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.14 05:07:49