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.