Vitest测试renderSetupPage时,按钮点击后startGame Spy未触发问题
问题分析与解决方案
问题原因
- 内部函数引用不匹配:
renderSetupPage里的按钮点击事件直接调用的是模块作用域内的startGame函数,而非你通过utils.startGame访问的导出函数。你用vi.spyOn(utils, 'startGame')mock的是导出的函数,不会影响模块内部的直接调用。 - Mock时机错误:你在
describe块中提前执行了utils.renderSetupPage(container),此时按钮的onclick已经绑定了原始的startGame函数,后续的mock无法修改这个已绑定的引用。
解决方案
方案一:调整源码,让内部调用导出的函数
修改../scripts/utils.js中的按钮点击逻辑,改为调用导出的startGame:
export const renderSetupPage = (container) => { // 创建父元素部分不变 const div = document.createElement("div") div.innerHTML = ` <label for="seed">Seed:</label> <input type="text" id="seed" name="seed"> ` // 创建按钮部分修改点击事件 const button = document.createElement("button") button.id = "start-game-button" button.innerText = "Start Game" // 改为调用导出的startGame button.onclick = () => exports.startGame() div.appendChild(button) container.appendChild(div) } export const startGame = () => { window.alert("game started") }
然后调整测试代码,确保先mock再渲染页面,且每个测试独立初始化DOM:
import { test, expect, vi, describe } from "vitest"; import * as utils from '../scripts/utils'; import {JSDOM} from "jsdom" describe.only("that setup page is build correctly", ()=>{ // 每个测试重新创建DOM环境 const getContainer = () => { const dom = new JSDOM("<div id='container'></div>") global.document = dom.window.document return document.getElementById("container") } test("that create button exists",() => { const container = getContainer() utils.renderSetupPage(container) const button = container.getElementsByTagName("button") expect(button.length).toBe(1) expect(button[0].id).toBe("start-game-button") }) test("that button click starts game",()=>{ // 先mock导出的startGame const startGameSpy = vi.spyOn(utils, 'startGame').mockImplementation(()=>{}) const container = getContainer() // mock后再渲染页面,确保按钮绑定mock后的函数 utils.renderSetupPage(container) const button = container.getElementsByTagName("button") button[0].click() expect(startGameSpy).toHaveBeenCalled() // 恢复原始函数,避免影响其他测试 startGameSpy.mockRestore() }) })
方案二:不修改源码,用模块mock实现测试
如果不想改动业务代码,可以用Vitest的vi.mock对整个模块做partial mock,保留原始的renderSetupPage,替换startGame为mock函数:
import { test, expect, vi, describe } from "vitest"; import {JSDOM} from "jsdom" // Partial mock:保留原始的renderSetupPage,mock startGame vi.mock('../scripts/utils', async () => { const originalModule = await vi.importActual('../scripts/utils'); return { ...originalModule, startGame: vi.fn() } }); import * as utils from '../scripts/utils'; describe.only("that setup page is build correctly", ()=>{ const getContainer = () => { const dom = new JSDOM("<div id='container'></div>") global.document = dom.window.document return document.getElementById("container") } test("that create button exists",() => { const container = getContainer() utils.renderSetupPage(container) const button = container.getElementsByTagName("button") expect(button.length).toBe(1) expect(button[0].id).toBe("start-game-button") }) test("that button click starts game",()=>{ const container = getContainer() utils.renderSetupPage(container) const button = container.getElementsByTagName("button") button[0].click() // 断言mock的startGame被调用 expect(utils.startGame).toHaveBeenCalled() }) })
内容的提问来源于stack exchange,提问作者Jan Seipel
相关产品推荐
相关产品推荐

