Search code examples
node.jssequelize.jschaisinonstubbing

Stubbing Models contained within an Object


I am having real issues stubbing one particular thing using sinon. I have a simple function I am testing

const floatAPIModels = require("models/float/floatAPIModels");

const insertFloatData = async (databaseConnection, endpoint, endpointData) => {
  try {
    const floatModel = floatAPIModels(databaseConnection);
    await databaseConnection.sync();

    if (endpoint === "people") {
      endpointData.forEach(async (record) => {
        await floatModel.Person.upsert(record);
      });
    }

    return true;
  } catch (error) {
    console.log("Unable to insert data into the database:", error);
    return error;
  }
};

The problem is with floatAPIModels being an Object that returns things. My implementation is this

const { DataTypes } = require("sequelize");

const floatAPIModels = (sequelize) => {
  const Person = sequelize.define(
    "Person",
    {
      people_id: { type: DataTypes.INTEGER, primaryKey: true },
      job_title: { type: DataTypes.STRING(200), allowNull: true },
      employee_type: { type: DataTypes.BOOLEAN, allowNull: true },
      active: { type: DataTypes.BOOLEAN, allowNull: true },
      start_date: { type: DataTypes.DATE, allowNull: true },
      end_date: { type: DataTypes.DATE, allowNull: true },
      department_name: { type: DataTypes.STRING, allowNull: true },
      default_hourly_rate: { type: DataTypes.FLOAT, allowNull: true },
      created: { type: DataTypes.DATE, allowNull: true },
      modified: { type: DataTypes.DATE, allowNull: true },
    },
    {
      timestamps: true,
      tableName: "Person",
    }
  );

  return {
    Person,
  };
};

module.exports = floatAPIModels;

I have removed some things to cut down on code. At the moment I am doing something like this

const { expect } = require("chai");
const sinon = require("sinon");
const floatAPIModels = require("src/models/float/floatAPIModels");
const floatService = require("src/services/float/floatService");

describe("insertFloatData", () => {
  let databaseConnection;
  let floatModelMock;

  beforeEach(() => {
    databaseConnection = {};

    floatModelMock = {
      Person: { upsert: sinon.stub().resolves() },
    };

    sinon.stub(floatAPIModels, "Person").returns(floatModelMock.Person);
  });

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

  it("should insert endpointData into the 'people' endpoint", async () => {
    const endpoint = "people";
    const endpointData = [{ record: "data" }];

    await floatService.insertFloatData(databaseConnection, endpoint, endpointData);

    expect(floatModelMock.Person.upsert.calledOnce).to.be.true;
    expect(floatModelMock.Person.upsert.firstCall.args[0]).to.deep.equal(endpointData[0]);
  });
});

With the above, I get

TypeError: Cannot stub non-existent property Person

But I have tried default, and a lot of other ways, but none of them seems to work.

How can I properly stub this and get the unit test working?

Thanks


Solution

  • floatAPIModels is a function that returns { Person } object. There is no Person property on this function. That's why you got the error.

    In order to stub the floatAPIModels function, I will use the proxyquire module to do this.

    E.g.

    model.js:

    const { DataTypes } = require("sequelize");
    
    const floatAPIModels = (sequelize) => {
      const Person = sequelize.define(
        "Person",
        {
          people_id: { type: DataTypes.INTEGER, primaryKey: true },
          // rest fields, don't matter for this test
          // ...
        },
        { timestamps: true, tableName: "Person", }
      );
    
      return {
        Person,
      };
    };
    
    module.exports = floatAPIModels;
    

    service.js:

    const floatAPIModels = require("./model");
    
    const insertFloatData = async (databaseConnection, endpoint, endpointData) => {
      try {
        const floatModel = floatAPIModels(databaseConnection);
        await databaseConnection.sync();
        if (endpoint === "people") {
          endpointData.forEach(async (record) => {
            await floatModel.Person.upsert(record);
          });
        }
        return true;
      } catch (error) {
        console.log("Unable to insert data into the database:", error);
        return error;
      }
    };
    
    module.exports = { insertFloatData }
    

    service.test.js:

    const sinon = require("sinon");
    const proxyquire = require('proxyquire');
    
    describe("insertFloatData", () => {
      let databaseConnection;
    
      beforeEach(() => {
        databaseConnection = {
          define: sinon.stub(),
          sync: sinon.stub()
        };
      });
    
      afterEach(() => {
        sinon.restore();
      });
    
      it("should insert endpointData into the 'people' endpoint", async () => {
        const endpoint = "people";
        const endpointData = [{ record: "data" }];
        const PersonStub = {
          upsert: sinon.stub()
        }
        const floatAPIModelsStub = sinon.stub().returns({ Person: PersonStub })
        const floatService = proxyquire('./service', {
          './model': floatAPIModelsStub
        })
    
        await floatService.insertFloatData(databaseConnection, endpoint, endpointData);
    
        sinon.assert.calledOnce(PersonStub.upsert)
        sinon.assert.match(PersonStub.upsert.firstCall.args[0], endpointData[0])
      });
    });
    

    Test result:

      insertFloatData
        ✓ should insert endpointData into the 'people' endpoint (4168ms)
    
    
      1 passing (4s)
    
    ------------|---------|----------|---------|---------|-------------------
    File        | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
    ------------|---------|----------|---------|---------|-------------------
    All files   |   76.47 |       50 |   66.67 |   76.47 |                   
     model.js   |      60 |      100 |       0 |      60 | 4-24              
     service.js |   83.33 |       50 |     100 |   83.33 | 14-15             
    ------------|---------|----------|---------|---------|-------------------