⚙️ Integration Tests on Node.js CLI: Part 4 — Mocking Services
This article is part of a series about writing Node.js CLI programs and more specifically, testing them by writing E2E/Integration tests, rather than writing the usual module unit test. If you want to skip to the final implementation code, check here. The links to the other parts are listed below:
- Part 1: Why and how?
- Part 2: Testing interaction / User input
- Part 3: Inter Process Communication
- Part 4: Mocking Services
When unit testing applications, it is quite common to avoid hitting real services or endpoints, for many reasons: the most obvious, because it’s supposed to be a unit test, meant to test units or pieces of functionality. But also because running tests against a real service might compromise the quality of the service, due to stressing it with extra throughput, discrepancies against real data or even because of the maintenance cost of having a test API running only for testing. A common workaround is to spin up a test API with Docker in our CI/CD solution, that lives only through the tests duration. But often times working this setup out is really time consuming, and in some cases, there’s too much interdependency between services, turning this task into quite an undertaking. So unit tests with mocked services, usually with fixed data gathered from the real service seems like the only sane choice. It’s obvious that unit tests are supposed to be the cheap ones running faster, but that doesn’t mean we don’t want our E2E tests to run fast. Wouldn’t it be nice if we can have certain elements of our architecture mocked for our integration tests?
I know some of you might have read Eric Elliot’s famous (and very much recommended if you haven’t) Mocking is a Code Smell and are wondering if really mocking services is the way to go. In this case, I’d like to highlight two considerations for our specific case:
- As stated in Eric’s post, Mocking is required when our decomposition strategy has failed. This means that we can’t decompose any further because, well, we’re testing our integration between the CLI modules and the services. In fact, he does mention that mocking is ok for integration tests.
- Technically speaking, we’re not entirely mocking any interface or dependency on our CLI tool, as we’re providing a mechanism to deliver fixed/sample data to our app. We could argue, though that said mechanism is in fact a mock, but it’s intended to be bypassed by the actual tool, so we’re not hijacking or replacing parts of the functionality, but rather providing the tool with means to gather data from a different source.
Why can’t we just mock the correct modules (like request or http) and intercept calls directly? Remember that for our case, we’re in a different (parent) process. That means that the child process doesn’t share state and resources with its parent, unlike unit tests, in which the target module and the test runner live under the same Node.js process. In E2E testing, the application as a whole is treated as a black box and we’re only allowed to interact with the public interface we provided. That means that accessing parts of the app is not really a choice.
The workaround
Back to my use case, I could in theory send the fixed data down to the process and add a flag to the module in charge of requesting data, to respond with a fixture if provided:
// Mocking Data through IPC
// Place this in your entry fileif (process.env.NODE_ENV === 'test') { // This can be done better, just for the sake of example
global.MOCK_DATA = global.MOCK_DATA || []; process.on('message', msg => {
if (msg.mock) {
global.MOCK_DATA.push({ // The interface here is similar to mock libraries
// like nock: send a Regular Expression to test URLs
// against and if matches, send the mock response
match: msg.mock
? msg.mock instanceof RegExp
? msg.mock
: new RegExp(msg.mock, 'i')
: /.*/gi,
method: msg.method,
response: {
status: msg.response.status,
data: msg.response.data
}
});
}
});
}
For this approach to work, you might need to create a proxy method from which to intercept API calls and send data if there’s any matching in global.MOCK_DATA
, similar to a cache pattern:
// request_handler.js// Replace here with the request library of your choice
const axios = require('axios');module.exports = async function request(args) {
if (
process.env.NODE_ENV === 'test' &&
Array.isArray(global.MOCK_DATA)
) {
let mockedData; global.MOCK_DATA.some(mock => {
// This match is simplified for example purposes
// implement it better based on your own needs.
if (
args.method === mock.method &&
mock.match.test(args.url)
) {
mockedData = mock.response;
return true;
}
});
if (mockedData) {
return mockedData;
}
} // Default to real request if no mock found
return axios(args);
}
The last part would be to add fixtures to the test and pin all the loose ends:
// test_runner.js
const cmd = require('./cmd');describe('Test my process', () => {
it('should print the correct output', async () => {
const promise = cmd.execute('my_process.js'); const childProcess = promise.attachedProcess;
// Send the response you expect from the service
childProcess.send({
mock: /^\/api\/endpoint$/, // Endpoint to match
method: 'GET', // Target method to mock
response: { data: 'IPC works!' },
});
// Remember to wait until the mock is in place
// to call the CLI command
childProcess.send('start');
const response = await promise; expect(response).to.equal(
'Response is: { data: 'IPC works!' }'
);
});
});
In conclusion
As we saw through the series, writing E2E tests for CLI applications is not as straightforward as it seems, but if done correctly can lead to a better software quality overall. Traditionally, E2E tests are done by QA Automation engineers, but considering that most CLI tools are to ease developers lives, and the main consumers of CLI apps are other developers, these kind of tests are often overlooked. Yet, if the CLI app gains enough traction, automated testing can ease the pain when delivering new, backwards compatible features to our tools, and ultimately deliver solid apps.
We started by testing simple input/output to test interaction and asynchronous services, with a full fledged mocking solution. I haven’t done my homework to check if there’s a better E2E testing suite in other programming languages — which by the way might better suited for this kind of task — and of course I encourage all of you to share your experiences with other solutions. Tell us in the comments how was your experience! For me, as a JavaScript developer, I feel that we’re short on this kind of testing for our tools, and boy, do we use CLI tools!
😱 I can’t believe it’s almost a year already since I posted the first part of this ambitious series. I really didn’t expect it to take me this long. I’ve been in different projects since then, experienced and learned from different tools and technologies, and of course I also want to share some of the knowledge I gathered through the course of this past year. In a way I feel relieved to finish this series. As they say, the last mile is always the longest. But of course you could have just happened to stumble upon this post recently, which is nice too. I hope some of the insights I shared here help you in your testing solution, and as always, don’t hesitate to call me out on any errors that you’ve seen. I’d be happy to talk about how you worked your testing solution out. Until next time! 🎉