Sinon stub not working with module.exports = { f1, f2}

Viewed 367

I have this file which sends otp like below.

OtpService.js

const generateOTP = async function() {
 //
}

const verifyOTP = async function() {
//
}

module.exports = {
 generateOTP,
 verifyOTP
}

Below is the controller that uses these methods, otp.js

const { verifyOTP, generateOTP } = require('../../services/OtpService')

const verify = async function(req, res) {
 const {error, data} = await generateOTP(req.query.phone)
}

const send = async function(req, res) {
 const {error, data} = await verifyOTP(req.query.phone, req.query.otp)
}

module.exports = {
 send,
 verify
}

below is the test file otp.test.js

const sinon = require('sinon');
const expect = require('chai').expect
const request = require('supertest')
const OtpService = require('../../src/services/OtpService')
console.log(OtpService)
describe('GET /api/v1/auth/otp', function() {
  let server 
  let stub
  const app = require('../../src/app')
  stub = sinon.stub(OtpService, 'generateOTP').resolves({
    error: null,
    data: "OTP Sent"
  })
  server = request(app)
  it('should generate OTP', async () => {
    const result = await server
        .get('/api/v1/auth/otp/send?phone=7845897889')
        .set('Accept', 'application/json')
        .expect('Content-Type', /json/)
        .expect(200)
        console.log(result.body)
    expect(stub.called).to.be.true
    expect(result).to.be.a('Object')
  });
});

Above is not working, it's not stubbing the generateOTP and verifyOTP methods when called in the controller.

However, If I call OtpService.generateOTP() in the otp.test.js then it is working there, but it doesn't work in the controller.

Hhow sinon is working here?

I am confused here. Does requiring the app and then stubbing is correct or stubbing first and then requiring is correct?

I have tried both ways though neither of them work. I also tried using before() and beforeEach().

Below is my folder structure.

enter image description here

otp.js(controller) is here controller->AuthController->otp.js

otp.test.js is here test->auth->otp.test.js

OtpService.js is just inside services

Update

I found the the problem. If I don't use destructing feature in the controller everything works fine. So, using OtpService.generateOTP works.

Problem is with the destructing of the objects.

const { verifyOTP, generateOTP } = require('../../services/OtpService')

Above is being run before the stubbing. So verifyOTP and generateOTP already has a reference to the unstubbed methods.

I need a workaround here. I want to use destructing feature.

2 Answers

I use proxyquire package to stub OtpService module. The below example is unit test, but you can use this way for your integration test.

E.g.

otp.js:

const { verifyOTP, generateOTP } = require('./OtpService');

const verify = async function(req, res) {
  return generateOTP(req.query.phone);
};

const send = async function(req, res) {
  return verifyOTP(req.query.phone, req.query.otp);
};

module.exports = {
  send,
  verify,
};

OtpService.js:

const generateOTP = async function() {
  //
};

const verifyOTP = async function() {
  //
};

module.exports = {
  generateOTP,
  verifyOTP,
};

otp.test.js:

const proxyquire = require('proxyquire');
const sinon = require('sinon');

describe('60704684', () => {
  it('should send', async () => {
    const otpServiceStub = {
      verifyOTP: sinon.stub().resolves({ error: null, data: 'fake data' }),
      generateOTP: sinon.stub(),
    };
    const { send } = proxyquire('./otp', {
      './OtpService': otpServiceStub,
    });
    const mReq = { query: { phone: '123', otp: 'otp' } };
    const mRes = {};
    await send(mReq, mRes);
    sinon.assert.calledWithExactly(otpServiceStub.verifyOTP, '123', 'otp');
  });

  it('should verfiy', async () => {
    const otpServiceStub = {
      verifyOTP: sinon.stub(),
      generateOTP: sinon.stub().resolves({ error: null, data: 'fake data' }),
    };
    const { verify } = proxyquire('./otp', {
      './OtpService': otpServiceStub,
    });
    const mReq = { query: { phone: '123' } };
    const mRes = {};
    await verify(mReq, mRes);
    sinon.assert.calledWithExactly(otpServiceStub.generateOTP, '123');
  });
});

unit test results with coverage report:

  60704684
    ✓ should send (1744ms)
    ✓ should verfiy


  2 passing (2s)

---------------|---------|----------|---------|---------|-------------------
File           | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
---------------|---------|----------|---------|---------|-------------------
All files      |     100 |      100 |      50 |     100 |                   
 OtpService.js |     100 |      100 |       0 |     100 |                   
 otp.js        |     100 |      100 |     100 |     100 |                   
---------------|---------|----------|---------|---------|-------------------

source code: https://github.com/mrdulin/expressjs-research/tree/master/src/stackoverflow/60704684

The problem is when you import something try to stub any of its property, it actually stub the property of the imported object instead of the one you want to. which exist somewhere in other application.

I had similar issue, I moved my sendOtp function to Model. and import that model and stub sendOtp method. It worked!

in your User model or schema, add

Models/Users.js

Users.generateOtp = async function(){ // your sms sending code}

tests/yourtestfile.js

const {Users} = require("../yourModelsDirectory");
const sinon = require('sinon');

desribe("your test case", function(){

  const stub = sinon.stub(Users, "generateOtp"); // stub generateOtp method of Users model (obj)
  stub.returns(true); // make it do something

// your tests
it("do something", function(){// test code})

it("do another thing", function(){// test code})

after() {
  // resotre stubbed methods in last
  sinon.restore();
}

})

I recommend adding stubbing and restore part in before() and after() hooks.

let me know if it need more clearification.

Related