4

I have a Typescript class that uses InversifyJS and Inversify Inject Decorators to inject a service into a private property. Functionally this is fine but I'm having issues figuring out how to unit test it. I've created a simplified version of my problem below.

In the Jasmine unit test, how can I swap out the injected RealDataService with a FakeDataService? If the property wasn't private I could create the component and assign a fake service but I am wondering if this is possible by using the IOC Container.

I initially followed this this example in the InversifyJS recipes page but quickly realised that the container they created isn't used in any class under test. Also, most of the code examples that I can see in the InversifyJS docs don't cover how to unit test it.

Here is a simplified version of the problem:

myComponent.ts

import { lazyInject, Types } from "./ioc";
import { IDataService } from "./dataService";

export default class MyComponent {

    @lazyInject(Types.IDataService)
    private myDataService!: IDataService;

    getSomething(): string {
        return this.myDataService.get();
    }
}

dataService.ts

import { injectable } from "inversify";

export interface IDataService {
    get(): string;
}

@injectable()
export class RealDataService implements IDataService {
    get(): string {
        return "I am real!";
    }
}

IOC Configuration

import "reflect-metadata";
import { Container, ContainerModule, interfaces, BindingScopeEnum } from "inversify";
import getDecorators from "inversify-inject-decorators";

import { IDataService, RealDataService } from "./dataService";

const Types = {
    IDataService: Symbol.for("IDataService")
};

const iocContainerModule = new ContainerModule((bind: interfaces.Bind) => {
    bind<IDataService>(Types.IDataService).to(RealDataService);
});

const iocContainer = new Container();
iocContainer.load(iocContainerModule);
const { lazyInject } = getDecorators(iocContainer);
export { lazyInject, Types };

Unit Tests

import { Container } from "inversify";
import { Types } from "./ioc";
import MyComponent from "./myComponent";
import { IDataService } from "./dataService";

class FakeDataService implements IDataService {
    get(): string {
        return "I am fake!";
    }
}

describe("My Component", () => {
    let iocContainer!: Container;
    let myComponent!: MyComponent;

    beforeEach(() => {
        iocContainer = new Container();
        iocContainer.bind(Types.IDataService).to(FakeDataService);

        // How do I make myComponent use this iocContainer?
        // Is it even possible?

        myComponent = new MyComponent();
    });

    it("should use the mocked service", () => {
        const val = myComponent.getSomething();
        expect(val).toBe("I am fake!");
    });
});
Gavin Sutherland
  • 1,496
  • 1
  • 20
  • 34

1 Answers1

1

I was able to solve this by importing a container from a different file. Using this method, you would write a different container for every combination of dependencies you want to inject into a test. For brevity, assume the code example with ninja warriors given by the Inversify docs.

// src/inversify.prod-config.ts
import "reflect-metadata";
import { Container } from "inversify";
import { TYPES } from "./types";
import { Warrior, Weapon, ThrowableWeapon } from "./interfaces";
import { Ninja, Katana, Shuriken } from "./entities";

const myContainer = new Container();
myContainer.bind<Warrior>(TYPES.Warrior).to(Ninja);
myContainer.bind<Weapon>(TYPES.Weapon).to(Katana);
myContainer.bind<ThrowableWeapon>(TYPES.ThrowableWeapon).to(Shuriken);

export { myContainer };
// test/fixtures/inversify.unit-config.ts
import "reflect-metadata";
import {Container, inject, injectable} from "inversify";
import { TYPES } from "../../src/types";
import { Warrior, Weapon, ThrowableWeapon } from "../../src/interfaces";

// instead of importing the injectable classes from src,
// import mocked injectables from a set of text fixtures.
// For brevity, I defined mocks inline here, but you would
// likely want these in their own files.

@injectable()
class TestKatana implements Weapon {
  public hit() {
    return "TEST cut!";
  }
}

@injectable()
class TestShuriken implements ThrowableWeapon {
  public throw() {
    return "TEST hit!";
  }
}

@injectable()
class TestNinja implements Warrior {

  private _katana: Weapon;
  private _shuriken: ThrowableWeapon;

  public constructor(
    @inject(TYPES.Weapon) katana: Weapon,
    @inject(TYPES.ThrowableWeapon) shuriken: ThrowableWeapon
  ) {
    this._katana = katana;
    this._shuriken = shuriken;
  }

  public fight() { return this._katana.hit(); }
  public sneak() { return this._shuriken.throw(); }

}

const myContainer = new Container();
myContainer.bind<Warrior>(TYPES.Warrior).to(TestNinja);
myContainer.bind<Weapon>(TYPES.Weapon).to(TestKatana);
myContainer.bind<ThrowableWeapon>(TYPES.ThrowableWeapon).to(TestShuriken);

export { myContainer };
// test/unit/example.test.ts
// Disclaimer: this is a Jest test, but a port to jasmine should look similar.

import {myContainer} from "../fixtures/inversify.unit-config";
import {Warrior} from "../../../src/interfaces";
import {TYPES} from "../../../src/types";

describe('test', () => {
  let ninja;

  beforeEach(() => {
    ninja = myContainer.get<Warrior>(TYPES.Warrior);
  });

  test('should pass', () => {
    expect(ninja.fight()).toEqual("TEST cut!");
    expect(ninja.sneak()).toEqual("TEST hit!");
  });
});
  • You could also just define the container inside the same test file, so that you can have unique mock objects per test suite. – pypmannetjies Aug 27 '19 at 12:25
  • I think the question was about @lazyInject, which is tied to a particular container instance, so you can't switch it with another one in the test. I guess the only way to deal with it is to move loading of container modules to a separate function, which would run only in production, while using a separate configuring function of the same container instance for the tests. – Alexander Sheboltaev Feb 05 '20 at 15:07