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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.17 06:57:06