Mocklets
Reusable standard mocks and fakes for popular browser and Node.js APIs, framework/library objects for Jest
Install / Use
/learn @codesplinta/MockletsREADME
mocklets
This package helps you write tests using Jest (and Vitest - coming soon!) with much less boilerplate than you are currently used to. Much of this boilerplate is in the mocks you have to setup and use in your tests. It also helps to reduce friction with test assertions for side effects (or outputs) that are harder to assert on. By creating fakes and/or mocks that are reusable and also pass as Jest stubs making it easy to assert on them.
Therefore, mocklets is a set of reusable standard mocks (and fakes) for popular browser APIs, Node.js APIs and framework/library objects for Jest. This library creates a seamless bridge between Jest, JSDOM and Popular Thrid-party JS libraries (e.g. fetch(), localStorage, console.log(), window.location.href, window.open(), window.confirm(), new ResizeObserver(), MUI, Nextjs, react-hook-form, ExpressJS e.t.c) used for building apps such that you don't have to think about how and what you need/require to setup your testing space to write tests.
You can now write your Jest tests a lot more faster and better than before.
Preamble
So, mocklets as stated earlier is simply something that helps removes unnecessary heavy boilerplate and assertion friction when writing tests in Jest. But why and how does it help to remove all these ?
When writing tests, we have to assert on the side effects (or outputs) from our Jest test cases that has just run. Sometimes, it is easy to assert these side effects (or outputs) if they are in a place that is easy to access (e.g. window.localStoraget.getItem(...) if the output from our test case was set into local storage like using window.localStorage.setItem(...))
Other times, it is not so easy. We have to setup Jest stubs (i.e. testing tools that record when they are called, what they are called with and how many times they are called). Yet, when we want to verify these things from when third-party libraries/APIs are called, it's not easy because these third-party libraries/APIs are not wrapped with Jest #stubs that record these calls so we can easily assert the side effect from running our code in these test cases.
Plus, we don't always want to run the real thing (i.e. the real implementation of these third-party libraries/APIs) in our tests every time. We want to run a fake version or a mock version that behaves like the real implementation simply because it's cheaper to run. Also, since #Jest uses JSDOM which isn't a real browser environment, the real implementation might get stuck somehow.
So, mocklets is a bridge between faking out these third-party libries/APIs and making them test-friendly in a Jest/JSDOM environment.
For instance, window.location.href when set to a new url value in a real browser causes navigation so JSDOM doesn't really allow for it to be set to a new url value. But mocklets turns this around using some JS tricks make it possible to set window.location.href to a new url value making it easy to assert if window.location.href was changed during the running of your test case.
Again, mocklets also does the same for the useForm() hook from react-hook-form third-party library. Within your test environment, useForm() becomes a fake implementation that returns a Jest stub object that can asserted on.
Finally, mocklets does the same for the JS object returned by the NextJS hook: useRouter() when called. Now, you can easily assert that back() or push() was called since back() or push() are Jest stubs based on a fake implementation of useRouter().
Motivation
Everyone knows how hard software testing setup can be. When it comes to the testing pyramid or testing polygon, the most amount of work to be done is in creating fixtures or building mocks and fakes which can be quite daunting.
The very popular testing frameworks for unit testing and e-to-e tests are good at providing certain building blocks for creating mocks/fakes but how often do we have to rebuild/reconstruct the same building blocks to create the same exact (usually from scratch) materials in each test suite in order to make test doubles (e.g. mocks/stubs(spies)/fakes) available for different JavaScript software projects ?
This is where mocklets come in.
This project provides re-usable and standard mocks/stubs/fakes for Jest only (Vitest coming soon).
Finally, sometimes, Jest and JSDOM don't play nice and JSDOM has browser APIs that are not yet implemented or badly implemented (see list here). mocklets tries to shield you from these issues so you don't have to deal with them.
Installation
Install using
npm
npm install mocklets
Or install using yarn
yarn add mocklets
Support
mocklets can ONLY run well on Node.js v12.2.0 - v19.3.x as well as Jest v25.5.1 - v29.5.x
Getting Started
You can use mocklets inside your jest test suite files simply by importing into these files and calling the functions outside or within the describe() callback. You can also make addditional calls within any of test() and/or it() callbacks.
It is important to note that Jest module hoisting is still necessary for mocklets to work properly.
For instance, we can simply hoist React in Jest by doing this:
import React from 'react';
jest.mock('react', () => ({
...jest.requireActual('react')
}));
const $useRef = (value) => {
return {
current: value
}
};
jest.spyOn(require('react'), 'useRef').mockImplementationOnce(
$useRef
);
In the same vein, we can also provision react-hook-form hooks (when using mocklets) by doing this too:
import type { UseFormReturn, SubmitHandler } from 'react-hook-form';
import { render, fireEvent } from '@testing-library/react';
import {
provisionMockedReactHookFormForTests_withAddons
} from 'mocklets'
import Form from '../src/components/UI/regions/Form';
import { toBeArray, toBeEmpty } from 'jest-extended';
expect.extend({ toBeArray, toBeEmpty });
jest.mock('react-hook-form', () => ({
...jest.requireActual('react-hook-form')
}))
describe('Tests for my custom React form', () => {
const {
$setSpyOn_useForm_withMockImplementation
} = provisionMockedReactHookFormForTests_withAddons()
const stubSubmit = jest.fn() as unknown as SubmitHandler<{ id: number }>;
beforeEach(() => {
if ('mockClear' in stubSubmit
&& typeof stubSubmit['mockClear'] === 'function') {
stubSubmit['mockClear']();
}
});
it('should render the form', () => {
const { getByTestId } = render(
<Form onSubmit={stubSubmit} />
);
const form = getByTestId("my-form")
expect(form).toBeInTheDocument()
})
it('should submit the form', () => {
const { formState }: UseFormReturn = $setSpyOn_useForm_withMockImplementation({
options: {
values: {
id: 345458
}
}
});
const { getByTestId } = render(
<Form onSubmit={stubSubmit} />
);
const submitButton = getByTestId('submitbutton')
fireEvent.click(submitButton);
expect(formState.errors).not.toBeEmpty();
expect(stubSubmit).toHaveBeenCalled();
})
})
Avoiding Mocks
The philosophy which mocklets operates on is that of avoiding mocks (test doubles that have no implementation) for as long as possible before choosing them as a last resort. For mocklets, the real implementation (and interface) is prefered first. Where it is impractical to use the real implmentation and interface then, fakes (test doubles that have implementation) are prefered.
This layers of preference is due to the fact that tests have much hgher reliability when either the real implementation or a fake implmentation is used when testing.
Using a fake implementation boosts the confidence of a software engineer in the tests they've written so that it is safer and faster to make changes without worry.
Some Basic Example (on the browser)
src/greetingMaker/index.js
export default function greetingMaker (subjectFullName = 'John Doe', subjectTitle = 'Mr.') {
const format = window.localStorage.getItem('greeting:format');
const today = new Date();
const hourOfToDay = today.getHours();
let salutation = "Good evening";
if (hourOfToDay < 12) {
salutation = "Good morning";
}
if (hourOfToDay >= 12 && hourOfToDay <= 16) {
salutation = "Good afternoon";
}
if (format === "old-fashioned") {
salutation = "Good day";
}
return `${saluation}, ${subjectTitle} ${subj
