Search code examples
javascriptnode.jsunit-testingsinon

How to use ProxyRequire to mock NodeJs test


I am writing unit test cases for the following file

//filename: a.js

var amqp = require("amqplib");

class RMQ {
  constructor(connectionURI) {
    this.URI = connectionURI;
  }
  async getInstance() {
    var connection = await amqp.connect(this.URI);
    this.connection = connection;
    return connection;
  }
}

const RMQ_INSTANCE = new RMQ(process.env.RMQ_URL);

module.exports = {
  RMQ_INSTANCE,
};

I am using the instance RMQ_INSTANCE in below file

// filename: b.js

const { RMQ_INSTANCE } = require("./a");

module.exports.publishEmail = async function(message) {
  var connection = await RMQ_INSTANCE.getInstance();
  var channel = await connection.createChannel();
  var exchange = "some_exchange";
  var key = "some_key";
  var msg = JSON.stringify(message);
  await channel.assertExchange(exchange, "topic", { durable: false });
  await channel.publish(exchange, key, Buffer.from(msg));
  setTimeout(function () {
    connection.close();
  }, 500);
}

I am using proxyrequire to mock the RMQ_INSTANCE in b.js

// filename: b.test.js

var proxyrequire = require("proxyquire").noCallThru();
var sinon = require("sinon");
const { assert } = require("sinon");

class fakeRMQClass {
  constructor(connectionURI) {
    this.URI = connectionURI;
  }
  async getInstance() {
    var connection = getFakeRMQStub()
    return connection;
  }
}

var producerTest = function () {
  it("producer connection - success test", async function () {
    var fakeRMQInstance = new fakeRMQClass("fake_url");
    var rmqUtils = proxyrequire("../path/to/b.js", {
      "./a": fakeRMQInstance
    });
    await rmqUtils.publishEmail("fake_msg");
  });
  afterEach(function () {
    sinon.verifyAndRestore();
  });
};
describe("test_producer", producerTest);

But I see the mocking is not working correctly. Could anyone please help me to mock this correctly?


Solution

  • Since the RMQ_INSTANCE is an object, you can stub its methods using sinon.stub(obj, 'method'), you don't need to use proxyquire package.

    Since you want to test the b module, the b module only cares about the interface of the RMQ_INSTANCE it depends on, and the specific implementation does not matter.

    b.js:

    const { RMQ_INSTANCE } = require('./a');
    
    module.exports.publishEmail = async function (message) {
      var connection = await RMQ_INSTANCE.getInstance();
      var channel = await connection.createChannel();
      var exchange = 'some_exchange';
      var key = 'some_key';
      var msg = JSON.stringify(message);
      await channel.assertExchange(exchange, 'topic', { durable: false });
      await channel.publish(exchange, key, Buffer.from(msg));
      setTimeout(function () {
        connection.close();
      }, 500);
    };
    

    a.js:

    class RMQ {
      constructor(connectionURI) {
        this.URI = connectionURI;
      }
      async getInstance() {}
    }
    
    const RMQ_INSTANCE = new RMQ(process.env.RMQ_URL);
    
    module.exports = { RMQ_INSTANCE };
    

    b.test.js:

    const sinon = require('sinon');
    const { RMQ_INSTANCE } = require('./a');
    const { publishEmail } = require('./b');
    
    describe('test_producer', () => {
      let clock;
      before(() => {
        clock = sinon.useFakeTimers();
      });
      after(() => {
        clock.restore();
      });
      it('producer connection - success test', async () => {
        const channelStub = {
          assertExchange: sinon.stub().returnsThis(),
          publish: sinon.stub(),
        };
        const connectionStub = { createChannel: sinon.stub().resolves(channelStub), close: sinon.stub() };
        sinon.stub(RMQ_INSTANCE, 'getInstance').resolves(connectionStub);
        await publishEmail('fake message');
        sinon.assert.calledOnce(RMQ_INSTANCE.getInstance);
        sinon.assert.calledOnce(connectionStub.createChannel);
        sinon.assert.calledWithExactly(channelStub.assertExchange, 'some_exchange', 'topic', { durable: false });
        sinon.assert.calledWithExactly(
          channelStub.publish,
          'some_exchange',
          'some_key',
          Buffer.from(JSON.stringify('fake message')),
        );
        clock.tick(500);
        sinon.assert.calledOnce(connectionStub.close);
      });
    });
    

    test result:

      test_producer
        ✓ producer connection - success test
    
    
      1 passing (10ms)
    
    ----------|---------|----------|---------|---------|-------------------
    File      | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
    ----------|---------|----------|---------|---------|-------------------
    All files |     100 |      100 |      75 |     100 |                   
     a.js     |     100 |      100 |      50 |     100 |                   
     b.js     |     100 |      100 |     100 |     100 |                   
    ----------|---------|----------|---------|---------|-------------------