Vitest中如何正确模拟Leaflet模块的多层链式调用(map方法及返回对象的setView方法)
Vitest中如何正确模拟Leaflet模块的多层链式调用(map方法及返回对象的setView方法)
我来帮你梳理一下当前写法的问题,然后给出两种可行的正确模拟方案,解决你遇到的困惑~
先说说你当前写法的几个问题
- 嵌套
vi.mock是错误用法:你在vi.mock("leaflet")的工厂函数里又嵌套了vi.mock("leaflet.Map"),这不符合Vitest的mock规则——vi.mock是用来模拟整个模块的,嵌套会导致模块加载逻辑混乱,根本起不到你想要的效果。 - mock的结构和实际Leaflet不符:你让
leaflet.default.map返回的是mapMock函数本身,但实际Leaflet中map()调用后返回的是一个带有setView方法的实例对象,不是函数。 - 错误的实例创建方式:你尝试
new Map("")来获取实例,但你并没有正确模拟Leaflet.Map这个构造函数,自然会报错。
方案一:直接在模块mock中构建完整的链式结构
这种方式适合大多数场景,直接在顶层vi.mock里模拟出和Leaflet一致的结构:
import { vi, expect } from 'vitest'; import leaflet from 'leaflet'; // 导入你自己的业务模块 import { attachMap, MapConfiguration, GeographicCoordinates } from './your-module-path'; // 顶层mock Leaflet模块,Vitest会自动把这个mock提升到模块最顶端,确保导入leaflet时已经被mock vi.mock('leaflet', () => { // 先创建setView的mock函数 const mockSetView = vi.fn(); // 创建map方法的mock函数,返回带有setView的实例对象 const mockMap = vi.fn(() => ({ setView: mockSetView })); // Leaflet的默认导出是一个包含map方法的对象,所以我们返回对应的结构 return { default: { map: mockMap } }; }); describe('地图挂载测试', () => { beforeEach(() => { // 每次测试前重置所有mock的调用记录,避免测试间污染 vi.clearAllMocks(); }); it("可以将地图挂载到DOM", () => { const config = new MapConfiguration( "map", new GeographicCoordinates(38.0, -90.0), 15, 19 ); attachMap(config); // 验证map方法是否被正确调用,参数是DOM元素的id"map" expect(leaflet.map).toHaveBeenCalledWith("map"); // 验证setView是否被传入正确的坐标和缩放级别 const mapInstanceMock = leaflet.map.mock.results[0].value; expect(mapInstanceMock.setView).toHaveBeenCalledWith([38.0, -90.0], 15); }); });
这个方案的要点:
- 把
vi.mock放在模块顶层,Vitest会自动处理模块加载顺序,保证你导入的leaflet已经是mock后的版本。 - 完全复刻Leaflet的调用结构:
leaflet.map()返回一个带setView的对象,完美匹配你的业务代码逻辑。 - 用
vi.clearAllMocks()在每次测试前重置mock状态,确保测试的独立性。
方案二:动态mock实例(更灵活的场景)
如果你的测试需要更灵活的控制(比如某个测试要让setView抛出错误,测试异常处理),可以先简单mock模块,再在beforeEach里动态生成实例:
import { vi, expect } from 'vitest'; import leaflet from 'leaflet'; import { attachMap, MapConfiguration, GeographicCoordinates } from './your-module-path'; // 先简单mock Leaflet的map方法为一个空的mock函数 vi.mock('leaflet', () => ({ default: { map: vi.fn() } })); describe('地图挂载测试', () => { let mockSetView; let mockMapInstance; beforeEach(() => { // 每次测试前创建新的mock函数和实例 mockSetView = vi.fn(); mockMapInstance = { setView: mockSetView }; // 指定leaflet.map调用后返回我们的mock实例 leaflet.map.mockReturnValue(mockMapInstance); // 重置调用记录 vi.clearAllMocks(); }); it("可以将地图挂载到DOM", () => { const config = new MapConfiguration( "map", new GeographicCoordinates(38.0, -90.0), 15, 19 ); attachMap(config); expect(leaflet.map).toHaveBeenCalledWith("map"); expect(mockSetView).toHaveBeenCalledWith([38.0, -90.0], 15); }); // 示例:测试setView抛出错误的场景 it("处理setView抛出的错误", () => { // 修改mock让setView抛出错误 mockSetView.mockImplementation(() => { throw new Error("地图加载失败"); }); const config = new MapConfiguration( "map", new GeographicCoordinates(38.0, -90.0), 15, 19 ); // 这里可以测试你的业务代码是否正确处理了这个错误 expect(() => attachMap(config)).toThrow("地图加载失败"); }); });
这个方案的优势:
- 可以在不同测试中灵活修改mock的行为,满足多样化的测试场景。
- 实例和mock函数都是每次测试重新创建,避免了测试间的状态污染。
最后再提醒几个常见坑
- 不要在
vi.mock的工厂函数里做嵌套mock,也不要在里面写测试逻辑,它只负责定义模块的mock结构。 - 一定要保证mock的结构和实际模块一致,Leaflet的默认导出是一个对象,不是函数,这点很容易搞错。
- 记得重置mock的调用记录,否则前一个测试的调用记录会影响后一个测试的结果。
备注:内容来源于stack exchange,提问作者Chris Schmitz
相关产品推荐
相关产品推荐

