Let's get right into it. Our example involves the npm package 'pdfkit'. The way pdfkit is used in code looks something like this:
import pdfkit from 'pdfkit';
const doc = new pdfkit();
doc.text('This is page one of my pdf');
doc.addPage();
doc.text('This is page two of my pdf');
This is a very simple example which will create a pdf with two pages with some text on each page. There would be a little bit more work involved to actually create the pdf file, but for the purposes of this article, this example will suffice.
For my real life use case, I had some dynamic text that was being added to the pdf, so I wanted to make sure it was well unit tested. It's good practice to keep the boundaries of your unit test tight, so I didn't want to actually test the functionality of pdfkit here. It is enough to test that the methods detailed in their docs were being called as expected.
So if we're not going to use 'pdfkit' in our tests, that means we need to mock the dependency. This would normally be pretty easy — just add a jest.mock
and you're good to go. However, if you look again at how the import is being used, it is instantiated before any methods are called. This complicates our mock a lot. We could create an actual class with mocked methods, however, I found an approach which I feel is a lot easier:
jest.mock('pdfkit', () => {
const mockText = jest.fn();
const mockAddPage = jest.fn();
const mockDoc = {
text: mockText,
addPage: mockAddPage,
};
return {
_esModule: true,
default: jest.fn(() => mockDoc),
mockText,
mockPage,
});
We mock out our dependency as normal with jest.mock
, then create some mock functions which will replace the text
and addPage
methods. The mockDoc
object is essentially a mocked version of our instantiated class.
The value that we are returning from the callback is an object that will replace what our code under test will get with the import statement. The default value (which is what we're importing as pdfkit
will be a jest function which will return mockDoc
. The interesting part here is that we're including our mockText
and mockPage
functions. This is crucial as it allows us to import these functions into our test and make assertions against them:
import {mockText, mockAddPage} from 'pdfkit';
jest.mock('pdfkit', () => {
const mockText = jest.fn();
const mockAddPage = jest.fn();
const mockDoc = {
text: mockText,
addPage: mockAddPage,
};
return {
_esModule: true,
default: jest.fn(() => mockDoc),
mockText,
mockPage,
});
it('should call mockText twice', () => {
expect(mockText).toHaveBeenCalledTimes(2);
});
it('should add a new page once', () => {
expect(mockAddPage).toHaveBeenCalledTimes(1);
});
Without this import we would not be able to access these functions because the code under test creates a new instance of these functions when it calls new pdfkit()
.
Even if we were to import pdfkit in the same way and instantiate it, the methods would be different to the ones used in our code under test.
Self taught software developer with 11 years experience excelling at JavaScript/Typescript, React, Node and AWS.
I love learning and teaching and have mentored several junior developers over my career. I find teaching is one of the best ways to solidify your own learning, so in the past few years I've been maintaining a technical blog where I write about some things that I've been learning.
I'm passionate about building a teams culture and processes to make it efficient and satisfying to work in. In many roles I have improved the quality and reliability of the code base by introducing or improving the continuous integration pipeline to include quality gates.