Łukasz Makuch

Łukasz Makuch

How to mock POST requests using Nock in JavaScript

a person using a laptop

Nock is a popular JavaScript library used for server mocking.

Getting started with Nock is as easy as it gets. Do you want to mock a GET request? Simply write

nock('https://example.com')
  .get('/name').reply(200, "John")

It couldn't be easier to mock a GET request.

You can even mock multiple endpoints:

nock('https://example.com')
  .get('/name').reply(200, "John")
  .get('/surname').reply(200, "Doe")

Where it gets more tricky is mocking requests that are supposed to alter the state of the server, such as POST requests. It's very tempting, and in fact very common, to write a mock like this:

nock('https://example.com')
  .get('/name').reply(200, "John")
  .post('/name', { name: 'Johnny' }).reply(200)
  .get('/name').reply(200, "Johnny")

At the first glance, evertyhing works as expected:

console.log((await client.get('/name')).data) // John
await client.post('/name', { name: 'Johnny' }); // 200
console.log((await client.get('/name')).data) // Johnny

However, if we take a closer look, we may notice some discrepancies between our mock and how a real endpoint would behave. For instance, we'll get the updated name even if we don't actually update it!

console.log((await client.get('/name')).data) // John
// No POST request whatsoever
console.log((await client.get('/name')).data) // Johnny

It's because Nock doesn't have the notion of state. I'd even go as far as saying that Nock doesn't have a built-in support for POST requests. I know it may sound wrong, but hear me out.

When we register 3 mocks like that:

nock('https://example.com')
  .get('/name').reply(200, "John")
  .post('/name', { name: 'Johnny' }).reply(200)
  .get('/name').reply(200, "Johnny")

there are two unique requests: GET /name and POST /name. There are two responses associated with the GET /name request. They form a kind of First In First Out queue. And this queue is completely separate from the queue of POST /name!. It means that POST requests don't have any impact on what's returned in response to GET requests. The mock currently behaves in the same was as if we wrote it like this:

nock('https://example.com')
  .post('/name', { name: 'Johnny' }).reply(200) // queue A
  .get('/name').reply(200, "John") // queue B
  .get('/name').reply(200, "Johnny") // queue B 

or like that:

nock('https://example.com')
  .get('/name').reply(200, "John") // queue B
  .get('/name').reply(200, "Johnny") // queue B
  .post('/name', { name: 'Johnny' }).reply(200)  // queue A

If we focus on just the GET request, there are 2 and only 2 responses queued for it: "John" and "Johnny". That's why calling this mocked GET endpoint 3 times will give us consecutively: "John", "Johnny", and an error. That's unlike a real API, which in the same circumstances would return "John" three times.

It may let bugs slip in, if the system under test refetches the data before it updates it. Tests will pass, but only because the mock naively returns the updated data, even if it hasn't been updated. It will also make tests more fragile, as a relatively harmless superfluous GET request will significantly change the mocked data by shifting the queue associated with it.

Is all hope lost and we can forget about mocking POST requests in Nock? Fortunately, there IS a way to make Nock mimic a real server, so that we can write more reliable and robust tests! It can be accomplished through the means of functions:

// Define the state of the mocked server.
// In this example it's a name.
let name = 'John';

scope.persist()
    // In response to GET, return the name.
    .get('/name').reply(() => [200, name])
    // When we get a POST request
    .post('/name').reply((uri, body) => {
        // Change the state of the "server" 
        name = body.name;
        // Return 200.
        return [200];
    })

If you're familiar with express, it's kinda like writing mini express apps. And what advantages does it have over the naive solution with queues?

If we just call GET multiple times, we'll always get the same result:

console.log((await client.get('/name')).data) // John
console.log((await client.get('/name')).data) // John
console.log((await client.get('/name')).data) // John

But as soon as we fire up a POST request, the name changes:

console.log((await client.get('/name')).data) // John
console.log((await client.get('/name')).data) // John
console.log((await client.get('/name')).data) // John

await client.post('/name', { name: 'Johnny' });

console.log((await client.get('/name')).data) // Johnny
console.log((await client.get('/name')).data) // Johnny

Writing mocks in a way that closely resembles real APIs helps to catch bugs that could otherwise go unnoticed, i.e. if some component refetches data too early. It's also a pragmatic choice, because even if the component under test fires up some redundant requests, the tests won't fail as long as the observed behavior of the app is correct.

And if you're anything like me and you find this imperative style rather primitive and you're yearning for a more elegant solution, then have a look at HTTP mocking libraries with first-class support for state, such as WireMock or Endpoint Imposter.

From the author of this blog

  • howlong.app - a timesheet built for freelancers, not against them!