typescriptspyts-mockito

Why doesn't my ts-mockito spy delegate method calls?


I have a simple class called MyPresenter with a doOperation() method that calls one method on a View class that implements an interface and is passed in as a parameter. Here's the class, the interface and the 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}`);
  }
}

I have a test that creates a spy, invokes doOperation() on the spy and attempts to verify that the spy calls the displayMessage method on the view. When I run the test, the verify fails unless I replace the creation of the spy with a simple class instantiation. Here's the test:

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();
  });
});

This test fails with this message:

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)

If I comment out the bottom line in the beforeEach and uncomment the line above it (replacing the spy with a regular instance), the test passes, so it is clearly a problem with the spy not delegating the method call to the object it's spying on.

I realize I don't need to use a spy here, but this is a simplified version of a code example where I do need a spy to mock a factory method for a different dependency.

Any ideas why the spy does not delegate the method call?

Here are my package.json and tsconfig.json configuration files:

{
  "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"]
}

Solution

  • I found the solution. I needed to create an instance from the spy and call doOperation() on that instead of on the spy. The spy is used to setup and verify method calls. An instance created from the spy is used when you need to call methods on the spy or use it as a parameter. Here's the corrected code:

    import { MyPresenter, View } from "../../src/presenter/MyPresenter";
    import { anything, instance, mock, spy, when, verify } from "ts-mockito";
    
      describe("MyPresenter", () => {
      let presenterSpyInstance: MyPresenter;
      let mockView: View;
    
      beforeEach(() => {
        mockView = mock<View>();
        const mockViewInstance = instance(mockView);
    
        let presenterSpy = spy(new MyPresenter(mockViewInstance));
        presenterSpyInstance = instance(presenterSpy);
      });
    
      it("do operation", () => {
        presenterSpyInstance.doOperation();
        verify(mockView.displayMessage(anything(), anything())).once();
      });
    });