Unit tests are vital in software development. Especially when building a big application, we want to make sure that all its units work as expected.
By definition, unit tests help us ensure a single unit of code does what it has to do. Therefore, to create solid unit tests we must assure we're isolating the piece of code we want to test as much as possible. We don't want other modules (the required dependencies) to interfere with the outcome of the unit we're testing.
However, it could be a bit tricky to mock every single dependency a module requires. That's where Proxyquire comes into action.
Proxyquire is a handy module that lets us proxy all the dependencies required by the module we're testing when importing it. If we combine it with Sinon (another helpful Node JS module for testing), we can now stub with ease any dependencies, making it easy to focus on the behavior of the unit we're testing in isolation.
How it works
Let's use an example to illustrate how Proxyquire and Sinon work together. Imagine we need a module to retrieve/store user settings from/into a file for our application. Let's create that module along with its corresponding unit test.
The (simplified) code for this settings manager module would be as follows:
const fs = require('fs/promises');
module.exports = () => {
const load = async () => {
try {
const rawSettings = await fs.readFile('settings.json');
return JSON.parse(rawSettings);
} catch (error) {
return {};
}
};
const save = async (settings = {}) => {
const settingsStr = JSON.stringify(settings);
fs.writeFile('settings.json', settingsStr);
};
return {
load,
save
};
};
As we can see, our new module relies on Node JS' built-in file system module. Therefore, the behavior of this external module could affect the unit tests for the settings manager module we just created. Ideally, we don't want this to happen, because: 1) the behavior of the fs module (or any other module required by the unit being tested) could be completely out of our hands (say, a connection to an external API, database, file system, etc.), and 2) we want to focus our tests on a single unit (not its dependencies).
So, let's see how we can test our module by mocking its dependencies, avoiding unwanted behavior.
(Note: for this example, we're going to use Mocha + Chai as the test framework + library. Then, we're adding Sinon and Proxyquire for mocking the dependencies of the settings manager and testing it in isolation.)
First things first: to install all the dev dependencies we're going to use for testing we can use the following command:
npm install mocha chai sinon sinon-chai proxyquire
Then comes the code (some test cases are omitted for simplicity):
const chai = require('chai');
chai.use(require('sinon-chai'));
const { expect } = chai;
const sinon = require('sinon');
const proxyquire = require('proxyquire').noCallThru();
describe('Settings manager', () => {
const settingsPath = 'settings.json';
let settingsFake;
let fsFake;
let settingsManager;
beforeEach(() => {
settingsFake = {
language: 'English'
};
// [1] Create a fake 'fs' module with Sinon.
fsFake = {
// [2] Stub the 'readFile' function.
readFile: sinon.stub().callsFake(path => JSON.stringify(settingsFake)),
// [3] Stub the 'writeFile' function.
writeFile: sinon.stub().callsFake((path, settings) => {
settingsFake = JSON.parse(settings);
})
};
// [4] Load the settings manager module with Proxyquire.
settingsManager = proxyquire('../../settingsManager', {
'fs/promises': fsFake
})();
});
it('loads settings', async () => {
const settings = await settingsManager.load();
// Check that the stubbed function was called properly.
expect(fsFake.readFile).to.have.been.calledOnceWith(settingsPath);
// Check that the returned settings are correct.
expect(settings).to.be.an('object').that.deep.equals(settingsFake);
});
it('saves settings', async () => {
const defaultSettings = {
language: 'English'
};
const updatedSettings = {
language: 'Japanese'
};
const updatedSettingsStr = JSON.stringify(updatedSettings);
// First, check that the original settings are equal to the default ones.
let settings = await settingsManager.load();
expect(settings).to.be.an('object').that.deep.equals(defaultSettings);
// Then, update the settings...
await settingsManager.save(updatedSettings);
// ..and check that the stubbed function was called properly.
expect(fsFake.writeFile).to.have.been.calledOnceWith(settingsPath, updatedSettingsStr);
// Finally, check that the settings where updated correctly.
settings = await settingsManager.load();
expect(settings).to.be.an('object').that.deep.equals(updatedSettings);
});
});
Here's a quick explanation of what happens on the above code:
First, we create a fake fs module with Sinon (1) which contains stubbed versions of the functions we use in our module. We have a stubbed readFile function (2) with a simplified behavior: it receives a path as a parameter (which is completely ignored) and it just returns the stringified settings fake. We also have a stubbed writeFile function (3) with a simplified behavior as well: it receives a path and the settings to save as parameters and it just updates the settings fake with the parsed settings received. We're not interested in testing these fs functions for these tests, so we can simplify their behavior as we would expect it to be for our module to work properly.
Finally, we load our settings manager module with Proxyquire (4) to inject the stubbed dependencies, preventing the real ones to be loaded instead.
This way we can avoid unexpected behavior from the modules required by the unit being tested and focus on its functionality. We can now run our unit tests as many times as we want and expect the same results every time.
That's how we can mock dependencies for creating unit tests in Node JS with Proxyquire and Sinon!
Thanks for reading! Let me know if you have any thoughts or questions!
For further reading: