0

I'm trying to write unit testing code on my InversifyJS project. Route testing (using supertest) is working properly. Then try to write sinon stub,spy testing, but couldn't be successful. My sample code is given below:

DemoRoute

@injectable()
class DemoRoute implements IDemoRoute {
 private _demoController:IDemoController;

 constructor( @inject(TYPES.IDemoController) demoController:IDemoController ) {
   this._demoController = demoController;
 }

 create(req: Request, res: Response, next: NextFunction) {
   return this._demoController.create(req.body)
 }
}

Demo.test.ts

import "reflect-metadata";
import * as sinon from "sinon";
const sandbox = sinon.createSandbox();

describe.only("Demo Spec 2", () => {
   demoController = container.get<IDemoController>(TYPES.IDemoController);

   beforeEach((done) => {
      insertStub = sandbox.stub(demoController, 'create');
      done();
   });

   afterEach(() => {
     sandbox.restore();
   });

   it("Should call demo route url", async done => {
     demoRoute  = container.get<IDemoRoute>(TYPES.IDemoRoute);
     const stub =  insertStub.returns(Promise.resolve({ body: { name: "test xyz", code: "test abc"} }));
     const result = await demoRoute.create({body: demoData.validData}, {send: (params) => params});
     expect(stub).to.have.been.called; // throw error
     done();
   });
}

Error in unit test

UnhandledPromiseRejectionWarning: AssertionError: expected create to have been called at least once, but it was never called

How can I solve this problem? Thanks in advance.

Lin Du
  • 88,126
  • 95
  • 281
  • 483
sabbir
  • 2,020
  • 3
  • 26
  • 48

2 Answers2

2

The reason is the demoController instance injected to the DemoRoute and the demoController instance getting by below statement:

const demoController = container.get<IDemoController>(TYPES.IDemoController);

They are two different demoController instances. You just made a stub for the demoController instance in your test file.

After know this, we can make a stub for DemoController.prototype.create, it will be applied to all instances.

E.g.

demoRoute.ts:

import "reflect-metadata";
import { IDemoRoute, IDemoController } from "./interfaces";
import { NextFunction } from "express";
import { TYPES } from "./types";
import { inject, injectable } from "inversify";

@injectable()
export class DemoRoute implements IDemoRoute {
  private _demoController: IDemoController;

  constructor(@inject(TYPES.IDemoController) demoController: IDemoController) {
    this._demoController = demoController;
  }

  create(req: Request, res: Response, next: NextFunction) {
    return this._demoController.create(req.body);
  }
}

demo.test.ts:

import "reflect-metadata";
import sinon from "sinon";
import { TYPES } from "./types";
import { IDemoController, IDemoRoute } from "./interfaces";
import { container } from "./inversify.config";
import { DemoController } from "./demoController";
import { expect } from "chai";

const sandbox = sinon.createSandbox();

describe.only("Demo Spec 2", () => {
  const demoController = container.get<IDemoController>(TYPES.IDemoController);
  let insertStub: sinon.SinonStub<any, any>;
  beforeEach(() => {
    insertStub = sandbox.stub(DemoController.prototype, "create");
  });

  afterEach(() => {
    sandbox.restore();
  });

  it("Should call demo route url", async () => {
    const demoData = { validData: {} };
    const demoRoute = container.get<IDemoRoute>(TYPES.IDemoRoute);
    // different demoController instances
    expect(demoRoute["_demoController"]).not.to.be.equal(demoController);
    insertStub.returns(Promise.resolve({ body: { name: "test xyz", code: "test abc" } }));
    const nextStub = sinon.stub();
    const result = await demoRoute.create(
      { body: demoData.validData } as any,
      { send: (params) => params } as any,
      nextStub,
    );
    sinon.assert.calledOnce(insertStub);
  });
});

Unit test result with coverage report:

 Demo Spec 2
    ✓ Should call demo route url


  1 passing (25ms)

---------------------|----------|----------|----------|----------|-------------------|
File                 |  % Stmts | % Branch |  % Funcs |  % Lines | Uncovered Line #s |
---------------------|----------|----------|----------|----------|-------------------|
All files            |    96.08 |      100 |       80 |    95.56 |                   |
 demo.test.ts        |    95.83 |      100 |       80 |    95.45 |                31 |
 demoController.ts   |    85.71 |      100 |       50 |       80 |                 7 |
 demoRoute.ts        |      100 |      100 |      100 |      100 |                   |
 inversify.config.ts |      100 |      100 |      100 |      100 |                   |
 types.ts            |      100 |      100 |      100 |      100 |                   |
---------------------|----------|----------|----------|----------|-------------------|

The completed example source code: https://github.com/mrdulin/mocha-chai-sinon-codelab/tree/master/src/stackoverflow/59103140

Lin Du
  • 88,126
  • 95
  • 281
  • 483
  • Instead of stubbing DemoController.prototype, couldn't he use `sinon.createStubInstance(DemoController)` in tandem with the stub he creates from the instance he pulls from the container? This has worked for our team. – halecommarachel Apr 01 '20 at 18:09
2

We were able to get stubbing working by stubbing the methods of the instances pulled from the inversify container, plus using sinon.createStubInstance() to mock the constructor. That would look like this:

describe.only("Demo Spec 2", () => {
   demoController = container.get<IDemoController>(TYPES.IDemoController);
   demoControllerStub = sinon.createStubInstance(IDemoController) as SinonStubbedInstance<IDemoController>;
   beforeEach((done) => {
      insertStub = sinon.stub(demoController, 'create');
      done();
   });

   afterEach(() => {
     sandbox.restore();
   });

   it("Should call demo route url", async done => {
     demoRoute  = container.get<IDemoRoute>(TYPES.IDemoRoute);
     const stub =  insertStub.returns(Promise.resolve({ body: { name: "test xyz", code: "test abc"} }));
     const result = await demoRoute.create({body: demoData.validData}, {send: (params) => params});
     expect(demoControllerStub.create.calledOnce);
     done();
   });
}

Another way that works for us, which I prefer, is to stub the instance and unbind and bind the stubbed instance to the container. That would look like this:

describe.only("Demo Spec 2", () => {
   demoControllerStub = sinon.createStubInstance(IDemoController);

   container.unbind(IDemoController);
   container.bind(IDemoController).toConstantValue(demoControllerStub);

   beforeEach((done) => {
      demoControllerStub.create.reset();
      done();
   });

   it("Should call demo route url", async done => {
     demoRoute  = container.get<IDemoRoute>(TYPES.IDemoRoute);
     const stub =  insertStub.returns(Promise.resolve({ body: { name: "test xyz", code: "test abc"} }));
     const result = await demoRoute.create({body: demoData.validData}, {send: (params) => params});
     sinon.assert.calledWith(demoControllerStub.create, sinon.match({body: demoData.validData});
     done();
   });
}

halecommarachel
  • 124
  • 1
  • 9