Search code examples
javascriptnode.jsexpresssupertestproxyquire

testing node express endpoint and stub 3rd party api call


I have an express app like this:

server.js

const postsController = require('./controllers/posts_controller.js')
module.exports = app = express()
app.get('posts', postsController.index)

posts_controller.js

const post = require('./post')()

module.exports = {
  index: (req, res) => {
    post.getAll().then((posts) => {
      res.status(200).send(posts)
    }, (error) => {
      res.status(400).send('text')
    })
  }
}

post.js

module.exports = () => {
  const thirdPartyApi = require('third_party_api')
  return {
    get: () => {
      // do some stuff
      return thirdPartyApi.get().then((resp) => {
        // do some more stuff
        return Promise.resolve(resp)
      })
    }
  }
}

spec/posts_controller_spec.js

const app = require('../server')
const request = require('supertest')

describe('GET /posts', () => {
  it('should return a collection of posts', () => {
    request(app)
     .get('/posts')
     .end((_err, resp) => {
       expect(resp.status).toEqual(200)
     })
  })
})

My goal here is to stub out the thirdPartyApi.get(). I tried with proxyquire by adding this line to posts_controller_spec:

proxyquire('../posts_controller', {third_party_api: {
  get: () => { console.log('stubbed out get method'); }
})

This doesn't work because the server.js file is the file that requires the third_party_api again.


I could do something like this to test the controller:

const postsController = proxyquire('../posts_controller', {third_party_api: {
  get: () => { console.log('stubbed out get method'); }
})


postsController.index(req, res)

This second strategy doesn't feel right because now I have to stub req and res and now I'm bypassing the actual app instance.

Is there an easy way to do this, with proxyquire, or otherwise?


Solution

  • I realized what's going on, proxyquire is not actually messing up here.

    the file post.js exports a function, so when posts_controller.js requires() the post.js file, it executes the function and the require for third_party_api is evaluated again and the stub is wiped out.

    This is the "runtimeGlobal" scenario described here: https://www.npmjs.com/package/proxyquire#globally-override-require-during-module-runtime

    Solution is to fix up post.js so that it doesn't export a function:

    const thirdPartyApi = require('third_party_api')
    return {
      get: () => {
        // do some stuff
        return thirdPartyApi.get().then((resp) => {
          // do some more stuff
          return Promise.resolve(resp)
        })
      }
    }
    

    Now, as long as this happens before the app is initialized, the

    proxyquire('../posts_controller', {third_party_api: {
      get: () => { console.log('stubbed out get method'); }
    }) 
    

    Then third_party_api module that gets required inside the post controller is evaluated at load time and it gets cached as expected.