Search code examples
javascriptnode.jsmocha.jssinon

Stub a standalone module.exports function using rewire


I am trying to stub a module.exports function. But I have some trouble. I will give you a sudo code of the situation.

MyController.js

const sendOTPOnPhone = rewire('../../src/services/OtpService/sendOTPOnPhone')

module.exports = async function(req, res) {
         const { error, data } = await sendOTPOnPhone(req.query.phone) //this is I want to stub
         if(error)
           return return res.send(error)
         return res.send(data)
}

sendOTPService.js

module.exports = async function(phone) {
               const result = await fetch(`external-api-call`)
               if(result.status !== 'success')
                  return {
                          error: "Failed to send OTP!",
                          data: null
                  }
               return {
                      error: null,
                      data: result
               }
}

sendOTPTest.js

const expect = require('chai').expect
const request = require('supertest')
const sinon = require('sinon')
const rewire = require('rewire')
const sendOTPOnPhone = rewire('../../src/services/OtpService/sendOTPOnPhone')

const app = require('../../src/app')

describe('GET /api/v1/auth/otp/generate', function () {
    it('should generate OTP', async () => {
        let stub = sinon.stub().returns({
            error: null,
            data: "OTP sent"
        })
        sendOTPOnPhone.__set__('sendOTPOnPhone', stub)
        const result = await request(app)
            .get('/api/v1/auth/otp/generate?phone=8576863491')
            .set('Accept', 'application/json')
            .expect('Content-Type', /json/)
            .expect(200)
        expect(stub.calledOnce).to.be.true
        console.log(result.body)
        // expect(result).to.equal('promise resolved'); 
    })
})

Above test is failing, stub is not being called. I don't know what am I missing? If I do this in my sendOTPService:

const sendOTP = async function() {}
module.exports = {
  sendOTP
}

and this in the controller.

const { error, data } = sendOTPOnPhone.sendOTPOnPhone(req.query.phone)

It works.

But I import it like const {sendOTPOnPhone } = require('../sendOTPService') It doesn't work.

I am aware that destructing changes the reference of the object.
Can someone suggest a workaround?
Is it possible to achieve this using rewire? OR It can be done with proxyquire.
Please can someone suggest?


Solution

  • Here is the integration testing solution using proxyquire, you should use Globally override require.

    app.js:

    const express = require('express');
    const controller = require('./controller');
    
    const app = express();
    app.get('/api/v1/auth/otp/generate', controller);
    
    module.exports = app;
    

    controller.js:

    let sendOTPOnPhone = require('./sendOTPOnPhone');
    
    module.exports = async function(req, res) {
      const { error, data } = await sendOTPOnPhone(req.query.phone);
      if (error) return res.send(error);
      return res.send(data);
    };
    

    sendOTPOnPhone.js:

    module.exports = async function(phone) {
      const result = await fetch(`external-api-call`);
      if (result.status !== 'success')
        return {
          error: 'Failed to send OTP!',
          data: null,
        };
      return {
        error: null,
        data: result,
      };
    };
    

    sendOTP.test.js:

    const request = require('supertest');
    const sinon = require('sinon');
    const proxyquire = require('proxyquire');
    
    describe('GET /api/v1/auth/otp/generate', function() {
      it('should generate OTP', async () => {
        let stub = sinon.stub().resolves({
          error: null,
          data: { message: 'OTP sent' },
        });
        stub['@global'] = true;
        const app = proxyquire('./app', {
          './sendOTPOnPhone': stub,
        });
        const result = await request(app)
          .get('/api/v1/auth/otp/generate?phone=8576863491')
          .set('Accept', 'application/json')
          .expect('Content-Type', /json/)
          .expect(200);
        sinon.assert.calledOnce(stub);
        console.log(result.body);
      });
    });
    

    Integration test results with coverage report:

      GET /api/v1/auth/otp/generate
    { message: 'OTP sent' }
        ✓ should generate OTP (2373ms)
    
    
      1 passing (2s)
    
    -------------------|---------|----------|---------|---------|-------------------
    File               | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
    -------------------|---------|----------|---------|---------|-------------------
    All files          |   68.75 |       25 |      50 |   73.33 |                   
     app.js            |     100 |      100 |     100 |     100 |                   
     controller.js     |   83.33 |       50 |     100 |     100 | 5                 
     sendOTPOnPhone.js |      20 |        0 |       0 |      20 | 2-4,8             
    -------------------|---------|----------|---------|---------|-------------------
    

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