HTTP Сall Mocks that Maintain Themselves

Creation and maintenance of mocks are widely recognized to be the most painful and time-consuming part of test automation. But do they necessarily have to be?.. One of the most

5 years ago

Latest Post On building the enterprise-grade Node.js applications by Igor Savin

Creation and maintenance of mocks are widely recognized to be the most painful and time-consuming part of test automation. But do they necessarily have to be?..

One of the most common targets for mocking are external services. There are a variety of reasons why calling real services from tests is not a good idea: difficulties with ensuring deterministic execution and predictable availability being not the least of them. Let us consider our options for this case.

If your application follows a properly layered structure, and calls to external systems in it are done through dedicated API client services, mocking external calls is as easy as mocking entire methods of said services. However, this requires creating those mocks manually, and if there is any additional logic on the API client services present (e. g. data mapping) or there are any middleware or interceptors used in your system, they are not going to be triggered in the test. We can do better.

Nock is a mature and popular HTTP server mocking library for Node.js. It works by mocking Node.js http.request method, meaning that all of your application logic is going to be executed in its pristine form. Now we can have mocks and test entire flow end-to-end! However, we still need to create all necessary mocks manually. Can we do better?..

Apparently, yes. There is a less known feature of nock, called Nockback, that allows us to record and then playback mocks using the same code!


const nockBack = require('nock').back
const request = require('request')
nockBack.setMode('record')

nockBack.fixtures = __dirname + '/nockFixtures' //this only needs to be set once in your test helper

// this will make a real call and save it into fixture file on first execution
// and reuse the fixture on subsequent runs
nockBack('zomboFixture.json', nockDone => {
  request.get('http://zombo.com', (err, res, body) => {
    nockDone()
  })
})

So far, so good, right? However, there is a problem. You see, Nockback is not very smart, and by default, it records mocks for all calls and then expects everything to be mocked. And more often than not, this is not what we want, since if you send a request to the endpoint of your own service and then receive mocked response, your test becomes fairly useless. Is all hope for magical effortless mocks lost?!

Fear not. Yours truly wrote smart wrapper over nock, called nockback-harder. It allows to easily enable or disable mocking of local calls as well as provides a bunch of quality of life improvements. Let's see it in action!


import { NockbackHelper } from 'nockback-harder'
import * as nock from 'nock'

function initHelper(dirname: string, passThroughLocalCall: boolean = true): NockbackHelper {
  return new NockbackHelper(nock, dirname + '/nock-fixtures', { passThroughLocalCall })
}

  it('replay', async () => {
    const helper = initHelper(__dirname)
    helper.startRecording()

    await helper.nockBack('google.com-GET.json', async () => {
      // this will be recorded
      const response = await request.get('www.google.com') 
      expect(response.status).toBe(200)
      expect(response.text).toMatchSnapshot()

      // this will not be recorded, but if there are any calls to external
      // calls made while processing this local request, they will be recorded
      const localResponse = await request.get('localhost:4000') 
      expect(localResponse.status).toBe(200)
      expect(localResponse.text).toMatchSnapshot()
})
  })

What if something in the external system or in your call parameters changed and you need to regenerate mocks? No problem!

  it('overwrite', async () => {
    const helper = initHelper(__dirname)
    helper.startRecordingOverwrite()

    await helper.nockBack('google.com-GET.json', async () => {
      const response = await request.get('www.google.com')
      expect(response.status).toBe(200)
      expect(response.text).toMatchSnapshot()
    })
  })

What if you've modified some of the mocks (e. g. to simulate error cases) and don't want them to be overwritten with the rest of the mocks? No problem!

  it('overwrite some', async () => {
    const helper = initHelper(__dirname)
    helper.startRecordingOverwrite()

    await helper.nockBack('google.com-GET-internal-error.json', { doNotOverwrite: true }, async () => {
      const response = await request.get('www.google.com')
      expect(response.status).toBe(500)
      expect(response.text).toMatchSnapshot()
    })
      
    await helper.nockBack('google.com-GET.json', async () => {
      const response = await request.get('www.google.com')
      expect(response.status).toBe(200)
      expect(response.text).toMatchSnapshot()
    })      
  })

One current limitation is that gzipped responses are stored verbatim and hence are not easily modifiable. There is an in-progress PR for Nock to improve the situation.

Igor Savin

Published 5 years ago