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(); }); });
关键说明
- 提前Mock模块:在测试文件顶部用
vi.mock()Mock整个useDate模块,确保组件导入的是Mock版本而非真实实现。 - 获取Mock引用:从Mock模块导入
useDate,这样可以在测试用例中动态修改其返回值。 - 动态修改Mock实现:每个测试用例中,重新设置
useDate的Mock返回值,按需控制isDateSet的结果,然后重新挂载组件(因为<script setup>代码在组件挂载时执行,必须在挂载前修改Mock)。 - 匹配原Composable结构:Mock返回的对象必须包含原Composable导出的所有属性,且类型匹配,避免出现TypeError。
内容的提问来源于stack exchange,提问作者Inex
相关产品推荐
相关产品推荐

