ts-mockito icon indicating copy to clipboard operation
ts-mockito copied to clipboard

Interface mocks cannot be bound in inversify containers

Open nseniak-iziwork opened this issue 4 years ago • 5 comments

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

nseniak-iziwork avatar Nov 05 '21 14:11 nseniak-iziwork

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 avatar Nov 17 '21 06:11 NagRock

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

NagRock avatar Nov 17 '21 06:11 NagRock

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

MaxNamazov avatar Nov 17 '21 13:11 MaxNamazov

@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?

Diveafall avatar Nov 21 '23 22:11 Diveafall

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

weyert avatar Dec 04 '23 19:12 weyert