NagRock / ts-mockito

Mocking library for TypeScript
MIT License
969 stars 93 forks source link

Interface mocks cannot be bound in inversify containers #222

Open nseniak-iziwork opened 2 years ago

nseniak-iziwork commented 2 years ago

We use inversify for IOC (https://inversify.io/). Injecting mocks is an important testing use case for us. However, we found that interface mocks cannot be properly bound in inversify contexts.

Here's code combining inversify and ts-mockito that shows the problem:

import "reflect-metadata";
import { Container } from "inversify";
import { instance, mock } from "ts-mockito";

const container = new Container();

// Binding a class mock: works as expected
class Bar {}

const barMock = mock(Bar);
container.bind<Bar>("Bar").toConstantValue(instance(barMock));
const barMockGet = container.get<IFoo>("Bar"); // => barMock (expected)

// Binding an interface mock: doesn't work
interface IFoo {
  prop: string;
}

const ifooMock = mock<IFoo>();
container.bind<IFoo>("IFoo").toConstantValue(instance(ifooMock));
const ifooMockGet = container.get<IFoo>("IFoo"); // => null (should be ifooMock)

After looking into the inversify code, it seems that the problem is that inversify.isPromise incorrectly returns true for interface mocks, which makes invetsify try to resolve the mock, resulting in a null value:

import { isPromise } from "inversify/lib/utils/async";

const isBarMockPromise = isPromise(barMock); // => false (expected)
const isIfooMockPromise = isPromise(ifooMock); // => true (should be false)

The inversify code for isPromise is here: https://github.com/inversify/InversifyJS/blob/master/src/utils/async.ts

NagRock commented 2 years ago

ts-mockito is using Proxy for interfaces mocking. So when InversifyJS wants to check if then is a function:

function isPromise(object) {
    var isObjectOrFunction = (typeof object === 'object' && object !== null) || typeof object === 'function';
    return isObjectOrFunction && typeof object.then === "function";
}

ts-mockito immediately creates mock function for then - thats why InversifyJS thinks its a mock. And ts-mockito needs to do that because it cannot figure out if its a part of interface or not (interfaces are not available in runtime).

NagRock commented 2 years ago

As a work around you can do when(mockedFoo['then']).thenReturn(undefined); - but I think its ugly.

MaxNamazov commented 2 years ago

Faced this problem today. Currently resolved with tiny wrapper function based on @NagRock suggestion

function mockInterface<T>(): T {
  const mocked = mock<T>();
  when((mocked as any).then).thenReturn(undefined);
  return mocked;
}
Diveafall commented 7 months ago

@NagRock Just encountered the same problem, but unfortunately I didn't see this post soon enough so had to spend a lot of time debugging. Since then is so important for identifying a Promise perhaps we should include it in the list of excludedFunctionNames inside MockableFunctionsFinder?

weyert commented 7 months ago

Yeah, also spend a long time to figure this issue out.