ts-mockito spy为何不委托方法调用?求助排查
我有一个名为MyPresenter的简单类,其doOperation()方法会调用传入的View接口实现类的方法,相关代码如下:
export interface View { displayMessage: (message: string, duration: number) => void; } export class MyPresenter { private view: View; public constructor(view: View) { this.view = view; } public doOperation(): void { this.view.displayMessage("The message", 0); } }
import { View } from "./MyPresenter"; export class ViewImpl implements View { public displayMessage(message: string, duration: number) { console.log(`The message is: ${message}, with duration ${duration}`); } }
我编写了测试用例,创建spy对象并调用其doOperation()方法,尝试验证spy是否调用了view的displayMessage方法,但测试失败。测试代码如下:
import { MyPresenter, View } from "../../src/presenter/MyPresenter"; import { anything, instance, mock, spy, when, verify } from "ts-mockito"; describe("MyPresenter", () => { let presenterSpy: MyPresenter; let mockView: View; beforeEach(() => { mockView = mock<View>(); const mockViewInstance = instance(mockView); // presenterSpy = new MyPresenter(mockViewInstance); presenterSpy = spy(new MyPresenter(mockViewInstance)); }); it("do operation", () => { presenterSpy.doOperation(); verify(mockView.displayMessage(anything(), anything())).once(); }); });
测试失败信息如下:
FAIL test/presenter/Test.test.ts ● MyPresenter › do operation Expected "displayMessage(anything(), anything())" to be called 1 time(s). But has been called 0 time(s). 16 | it("do operation", () => { 17 | presenterSpy.doOperation(); > 18 | verify(mockView.displayMessage(anything(), anything())).once(); | ^ 19 | }); 20 | }); 21 | at MethodStubVerificator.Object.<anonymous>.MethodStubVerificator.times (../node_modules/ts-mockito/src/MethodStubVerificator.ts:35:19) at MethodStubVerificator.Object.<anonymous>.MethodStubVerificator.once (../node_modules/ts-mockito/src/MethodStubVerificator.ts:20:14) at Object.<anonymous> (test/presenter/Test.test.ts:18:61)
若将spy替换为普通实例,测试则通过,显然是spy未委托方法调用导致的问题。注:本示例为简化场景,实际需用spy模拟其他依赖的工厂方法。
以下是我的package.json和tsconfig.json配置:
{ "name": "ts-mockito-spy-example", "private": true, "version": "0.0.0", "scripts": { "dev": "vite", "build": "tsc && vite build", "preview": "vite preview", "test": "jest" }, "devDependencies": { "typescript": "^5.0.2", "vite": "^4.4.5", "@babel/core": "^7.23.3", "@babel/preset-env": "^7.23.3", "@babel/preset-typescript": "^7.23.3", "@types/jest": "^29.5.8", "babel-jest": "^29.7.0", "jest": "^29.7.0", "ts-jest": "^29.1.1", "ts-mockito": "^2.6.1" } }
{ "compilerOptions": { "target": "ES2020", "useDefineForClassFields": true, "module": "ESNext", "lib": ["ES2020", "DOM", "DOM.Iterable"], "skipLibCheck": false, "esModuleInterop": true, /* Bundler mode */ "moduleResolution": "bundler", "allowImportingTsExtensions": true, "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, /* Linting */ "strict": true, "noFallthroughCasesInSwitch": true }, "include": ["src"] }
请问为何ts-mockito的spy不委托方法调用?
核心原因
ts-mockito的spy()函数默认不会自动将所有方法调用委托到原始实例,尤其是当类方法涉及私有字段(比如你的MyPresenter里的private view)时,spy的代理机制无法正确拦截并转发调用逻辑。你调用presenterSpy.doOperation()时,实际并没有执行原始实例里的代码,自然也就不会触发mockView.displayMessage。
另外,ts-mockito的spy设计初衷更偏向于捕获已有mock对象的调用,而非包裹真实实例保留原始行为,旧版本(比如你用的2.6.1)对类实例的代理支持存在局限性。
解决方案
方案1:用instance()包装spy对象
修改测试中的spy创建逻辑,把spy后的对象通过instance()转换,让代理能正确转发调用:
beforeEach(() => { mockView = mock<View>(); const mockViewInstance = instance(mockView); const presenterInstance = new MyPresenter(mockViewInstance); const presenterSpyObj = spy(presenterInstance); presenterSpy = instance(presenterSpyObj); // 关键步骤 });
方案2:显式存根需要保留行为的方法
如果只需要特定方法保留原始逻辑,可以用when()手动指定委托:
beforeEach(() => { mockView = mock<View>(); const mockViewInstance = instance(mockView); presenterSpy = spy(new MyPresenter(mockViewInstance)); // 显式让spy的doOperation调用原始逻辑 when(presenterSpy.doOperation()).thenCall(() => { presenterSpy['view'].displayMessage("The message", 0); }); });
方案3:升级ts-mockito版本
你当前使用的2.6.1版本较旧,3.x+版本对类实例spy的代理逻辑做了优化,大概率能解决这个问题。执行以下命令升级:
npm install ts-mockito@latest --save-dev
额外建议
如果实际场景需要模拟工厂方法,更推荐用mock()创建工厂的mock对象,再通过when()指定工厂方法的返回值,这种方式更符合测试隔离原则,也能避开spy代理的坑。
内容的提问来源于stack exchange,提问作者Jww

