logo logo

Mocking Dependencies in Integration Tests with Nock Part 2: Isolation and Reuse

Mocking Dependencies in Integration Tests with Nock Part 2: Isolation and Reuse

The previous article (part 1) introduced integration tests with Nock as a way to mock external HTTP requests. However, “Hello World” examples can leave coders stranded when working in an enterprise-level code repository. The following points demonstrate how to use this library in a more scalable way 🚀

Nock/Mock Factories

By wrapping all nock creation with a factory, the library can be isolated from the test code. The most basic form of this can be returning the base nock object, leaving the behavior up to the consuming test.

class EventsServiceMock {
  constructor(baseUrl) {
    this.baseUrl = baseUrl;
  }

  createReminderMock(overrides) {
    return nock(this.baseUrl).post("/v1/reminder");
  }
}


// Consuming Test
const eventsService = new EventsServiceMock(config.baseUrl);

it("creates a reminder", async function () {
  let eventApiCalled = false;

  eventsService.createReminderMock()
    .reply(201, (uri, requestBody) => {
      eventApiCalled = true;
      return { id: "some-uuid", other: "attribute" };
    });

  const response = await request(app)
    .post("/v1/todo")
    .set("Content-Type", "application/json")
    .set("Accept", "application/json")
    .send({
      title: "Make resolutions",
      reminder: {
        date: "2020-01-01T00:00:00Z",
      },
    });

  expect(response.status).to.equal(201);
  expect(eventApiCalled).to.be.true();
});

While this is a great first step, the creates a reminder test becomes aware of the event API’s Reminder contract. This might be acceptable if used in one place, but with tests scattered across a repository, consolidating into the EventsServiceMock class is less error-prone and easier to manage. Additionally, appropriate function names can arguably make the tests easier to read.

class EventsServiceMock {
  ...

  createReminderSuccessMock(overrides) {
    return nock(this.baseUrl)
      .post("/v1/reminder")
      .reply(201, { id: "some-uuid", other: "attribute", ...overrides });
  }

  createReminderErrorMock(overrides) {
    return nock(this.baseUrl)
      .post("/v1/reminder")
      .reply(500, "An error occurred");
  }
}

// Test File
const eventsService = new EventsServiceMock(config.baseUrl);

it("creates a reminder", async function () {
  const mockObject = eventsService.createReminderSuccessMock();

  ...

  // mockObject.isDone() is true if the nock was consumed
  // nock.isDone() is true if all nocks were consumed, best for afterEach() methods
  expect(mockObject.isDone()).to.be.true();
});

Consuming the factory instead of the nock library directly allows developers to insert additional logic as needed.

For example, a post and get call can be instantiated simultaneously.

const REMINDER_PROTOTYPE = {
  id: 'some-guid',
  // other props
};

class EventAPIMock {
  constructor(baseUrl) {
    this.baseUrl = baseUrl;
  }

  createReminderMock(overrides) {
    const combinedObject = {
      ...REMINDER_PROTOTYPE,
      ...overrides,
    };

    nock(this.baseUrl)
      .post("/v1/reminder")
      .reply(201, combinedObject);

    nock(this.baseUrl)
      .get(`/v1/reminder/${combinedObject.id}`)
      .reply(200, combinedObject)
      .persist();
  }
}

The Rule of Three

Just because these setups provide reusability doesn’t mean it’s right for all projects. Factories may be overkill for a TODO app. The rule of three says that if you duplicate code three times, it’s time to make it DRY.

Cleanup

The safest way to write integration tests with nock is to clean it entirely after each test. Leaving nocks intact can lead to test bleed-over, which is difficult to diagnose. Each test should be independent, with nocks setup each time, even if they are the same.

Most frameworks offer some sort of setup functions (usually beforeEach and afterEach). The global afterEach function is a great place to put nock.cleanAll(), which will remove any leftovers. Alternatively, including expect(nock.isDone()).to.be.true() helps keep strict compliance that only necessary nocks are created.

Directory Structure

There are many different approaches to a directory structure in a project, depending on the language or framework. These can vary from team to team, so multiple examples are included. By using Dan Abramov’s approach, teams can decide what is best for them, tweaking strategies in a way that makes sense for them.

Separating test code

src/
  services/
    aws.js
    events.js
    todo.js
test/
  services/
    aws.js
    events.js
    todo.js

Using the Separation of Concerns pattern, all outgoing HTTP calls are isolated into service classes that only deal with constructing a request and returning a response. This layer of the code does not deal with any other layer of the application (e.g., controllers, models, views, or routes). Using that same logic, the nock folder is isolated away from the src directory’s service classes.

Portability

src/
  services/
    aws/
      aws.js
      aws.nock.js
    events/
      events.js
      events.nock.js
    todo/
      todo.js
      todo.nock.js

While this approach blurs the separation of concerns regarding the directory structure, it benefits from keeping related code co-located. When copying services to another code repository or using them as prototypes for new ones, there is less chance that code and patterns are lost. Typically, these folders also have test files (aws.test.js) that are highly visible to developers.

💡 Be wary of accidentally including test artifacts in the final build if using a bundler or creating a docker image.

Conclusion

By building a factory abstraction around nock, teams can separate tests from contracts. As a result, refactoring and reuse become easier, and code becomes more readable. However, no one format works best for every project or team. Use these examples as a baseline and apply them to your own code.

 

About the author

Kevin Fawcett

Programming is my passion. I continuously pursue knowledge, regularly exploring new technologies, and methodologies. Over the years, I have collected experience with design patterns, best practices, and architecture that I enjoy teaching others. Mentoring reinforces my learning.

Leave a Reply

FacebookLinkedInTwitterEmail