Vitest中spyOn与Mock失效:实际实现提前执行致测试失败
问题:Vitest中Mock模块后仍调用真实实现导致断言失败
我在测试中用vi.spyOn对导入的useDate、useStorage及database模块做Mock,但运行测试时,useDate.getToday的真实返回值(比如2023-07-03)还是被传入测试模块,导致断言失败。调试发现getToday在spyOn执行前就已经被调用了,试了vi.mock也没解决。
测试文件代码
//Test file import * as database from '../../database' import * as useDateHook from '../useDate' import * as useStorageHook from '../useStorage' import { useGameStatusStorage } from '.' const getStorageMock = vi.fn() const setStorageMock = vi.fn() const getTodayMock = vi.fn() const isSameDateMock = vi.fn() const getDailyWordMock = vi.fn() describe('useGameStatusStorage', () => { afterEach(() => vi.clearAllMocks()) vi.spyOn(useStorageHook, 'useStorage').mockReturnValue({ getStorage: getStorageMock, setStorage: setStorageMock, }) vi.spyOn(useDateHook, 'useDate').mockReturnValue({ getToday: getTodayMock, isSameDate: isSameDateMock, }) vi.spyOn(database, 'getDailyWordFromDatabase').mockImplementation( getDailyWordMock ) it('Should call getStorage and setStorage with correct params when refreshGame is called', async () => { const todayMock = 'TODAY-MOCK' const dailyWordMock = 'DAILY-WORD-MOCKs' getTodayMock.mockReturnValue(todayMock) getDailyWordMock.mockReturnValue(dailyWordMock) useGameStatusStorage().refreshGame() expect(getStorageMock).toHaveBeenCalledTimes(1) expect(setStorageMock).toHaveBeenCalledTimes(1) expect(setStorageMock).toHaveBeenCalledWith({ guesses: [], gameFinishStatus: null, lastDate: todayMock, dailyWord: dailyWordMock, }) }) })
useGameStatusStorage文件代码
// useGameStatusStorage file import { getDailyWordFromDatabase } from '../../database' import { useDate } from '../useDate' import { useStorage } from '../useStorage' const { getToday } = useDate() const today = getToday() const defaultGuessStatistics: number[] = Array(GUESSES_AMOUNT).fill(0) const defaultGameStatusStorage: GameStatusStorage = { bestStreak: 0, guesses: [], guessStatistics: defaultGuessStatistics, totalGamesAmount: 0, winPercentage: 0, winStreak: 0, gameFinishStatus: null, winAmount: 0, lossAmount: 0, lastDate: today, dailyWord: getDailyWordFromDatabase(today), } export const useGameStatusStorage: () => UseGameStatusStorageHook = () => { const { getStorage, setStorage } = useStorage<GameStatusStorage>( defaultGameStatusStorage) const refreshGame = () => { const storage = getStorage() setStorage({ ...storage, guesses: [], gameFinishStatus: null, lastDate: today, dailyWord: getDailyWordFromDatabase(today), }) } return { refreshGame, } }
错误信息
FAIL src/hooks/useGameStatusStorage/useGameStatusStorage.test.ts > useGameStatusStorage > Should call getStorage and setStorage with correct params when refreshGame is called AssertionError: expected "spy" to be called with arguments: [ { guesses: [], …(3) } ] Received: 1st spy call: Array [ Object { "dailyWord": "DAILY-WORD-MOCKs", "gameFinishStatus": null, "guesses": Array [], - "lastDate": "TODAY-MOCK", + "lastDate": "2023-07-03", }, ]
解决方案
问题根源
问题出在useGameStatusStorage的模块级代码执行时机:
- 模块顶层直接调用了
useDate()和getToday(),这部分代码会在模块被导入时立即执行,早于测试文件中vi.spyOn的Mock设置。 - 当测试文件导入
useGameStatusStorage时,真实的getToday已经被调用,today变量已被赋值为真实日期,后续Mock无法覆盖这个预初始化的值。
方案1:将模块级变量移到函数内部
把today和defaultGameStatusStorage的初始化逻辑移到useGameStatusStorage函数内部,确保每次调用hook时才执行这些逻辑,让Mock能生效:
修改后的useGameStatusStorage代码:
// useGameStatusStorage file import { getDailyWordFromDatabase } from '../../database' import { useDate } from '../useDate' import { useStorage } from '../useStorage' const defaultGuessStatistics: number[] = Array(GUESSES_AMOUNT).fill(0) export const useGameStatusStorage: () => UseGameStatusStorageHook = () => { // 将初始化逻辑移到函数内部,每次调用hook时执行 const { getToday } = useDate() const today = getToday() const defaultGameStatusStorage: GameStatusStorage = { bestStreak: 0, guesses: [], guessStatistics: defaultGuessStatistics, totalGamesAmount: 0, winPercentage: 0, winStreak: 0, gameFinishStatus: null, winAmount: 0, lossAmount: 0, lastDate: today, dailyWord: getDailyWordFromDatabase(today), } const { getStorage, setStorage } = useStorage<GameStatusStorage>(defaultGameStatusStorage) const refreshGame = () => { const storage = getStorage() setStorage({ ...storage, guesses: [], gameFinishStatus: null, lastDate: today, dailyWord: getDailyWordFromDatabase(today), }) } return { refreshGame, } }
方案2:用vi.mock提前Mock模块(配合vi.mocked)
如果不想修改业务代码,可使用vi.mock在模块导入前就替换实现,确保模块初始化时使用Mock值:
修改后的测试文件代码:
// Test file import * as database from '../../database' import * as useDateHook from '../useDate' import * as useStorageHook from '../useStorage' import { useGameStatusStorage } from '.' // 提前Mock模块,确保导入时就使用Mock实现 vi.mock('../useDate', () => ({ useDate: vi.fn().mockReturnValue({ getToday: vi.fn(), isSameDate: vi.fn(), }), })) vi.mock('../useStorage', () => ({ useStorage: vi.fn().mockReturnValue({ getStorage: vi.fn(), setStorage: vi.fn(), }), })) vi.mock('../../database', () => ({ getDailyWordFromDatabase: vi.fn(), })) describe('useGameStatusStorage', () => { afterEach(() => vi.clearAllMocks()) it('Should call getStorage and setStorage with correct params when refreshGame is called', async () => { const todayMock = 'TODAY-MOCK' const dailyWordMock = 'DAILY-WORD-MOCKs' // 获取并配置Mock函数的返回值 const useDateMock = vi.mocked(useDateHook.useDate) useDateMock.mockReturnValue({ getToday: vi.fn().mockReturnValue(todayMock), isSameDate: vi.fn(), }) vi.mocked(database.getDailyWordFromDatabase).mockReturnValue(dailyWordMock) const useStorageMock = vi.mocked(useStorageHook.useStorage) const { getStorage, setStorage } = { getStorage: vi.fn(), setStorage: vi.fn(), } useStorageMock.mockReturnValue({ getStorage, setStorage }) useGameStatusStorage().refreshGame() expect(getStorage).toHaveBeenCalledTimes(1) expect(setStorage).toHaveBeenCalledTimes(1) expect(setStorage).toHaveBeenCalledWith({ guesses: [], gameFinishStatus: null, lastDate: todayMock, dailyWord: dailyWordMock, }) }) })
关键说明
- 模块级代码会在导入时立即执行,因此顶层调用的函数会早于测试中的Mock设置。
vi.spyOn是在模块导入后修改现有导出,无法影响已执行的模块级代码;vi.mock则是在模块导入前替换实现,能覆盖初始化逻辑。
内容的提问来源于stack exchange,提问作者Lerockit
相关产品推荐
相关产品推荐

