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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.06 10:11:01