It is possible to use jest.spyOn
for most of your mocking needs.
For instance, let's say we want to track how a user interacts with our website. If the user clicks on our banner, we will send a custom metrics event:
import sendMetric from "./send-metric";
const trackBannerClick = function () {
sendMetric({
eventType: "click.banner",
});
};
const Banner = () => (
<a href="..." onClick={trackBannerClick}>
<img src="..." />
</a>
);
For the implementation we will take the given eventType, and interact with the metrics service with the current time, and a unique marker showing the origin of the event:
src/metrics/index.js
import * as metricsService from "./metrics-service";
export default function ({ eventType }) {
metricsService.send({
event: `my_application_namespace.${eventType}`,
time: Date.now(),
});
}
Writing a test is very easy for this scenario. We can import the underlying
metrics API, directly use jest.spyOn
, and mock the functionality as required,
and assert that we have interacted with the API as intended:
src/metrics/__tests__/index.spec.js
import sendMetric from "../index";
import * as originalMetricsService from "../metrics-service";
describe("metrics-mocking-example", function () {
describe("when calling with a custom eventType", function () {
beforeEach(function () {
jest.spyOn(originalMetricsService, "send").mockReturnValueOnce(true);
sendMetric({ eventType: "click.banner" });
});
it("calls the metrics service as expected", function () {
expect(originalMetricsService.send).toHaveBeenCalledWith({
event: "my_application_namespace.click_banner",
time: 1523724949703,
});
});
});
});
The tests passed initially, but when we ran our tests again they failed:
It turns out that the hard-coded time stamp is the issue:
import sendMetric from "../index";
import * as originalMetricsService from "../metrics-service";
describe("metrics-mocking-example", function () {
describe("when calling with a custom eventType", function () {
beforeEach(function () {
jest.spyOn(originalMetricsService, "send").mockReturnValueOnce(true);
sendMetric({ eventType: "click.banner" });
});
it("calls the metrics service as expected", function () {
expect(originalMetricsService.send).toHaveBeenCalledWith({
event: "my_application_namespace.click_banner",
time: 1523724949703, });
});
});
});
There's many ways to fix this functionality. Firstly, if we do not care about the particular of the time, other than requiring it being a number:
import sendMetric from "../index";
import * as originalMetricsService from "../metrics-service";
describe("metrics-mocking-example", function () {
describe("when calling with a custom eventType", function () {
beforeEach(function () {
jest.spyOn(originalMetricsService, "send").mockReturnValueOnce(true);
sendMetric({ eventType: "click.banner" });
});
it("calls the metrics service as expected", function () {
expect(originalMetricsService.send).toHaveBeenCalledWith({
event: "my_application_namespace.click_banner",
time: expect.any(Number), });
});
});
});
If we care about the particular value, we can modify our test to include one of the following solutions:
// Use spyOn
jest.spyOn(Date, "now").mockReturnValueOnce(1234);
// Globally override time, remember jest is sandboxed
Date.now = function () {
return 1234;
};
Jest additionally provides a very powerful module mocking system. However, the semantics of this can be unexpected in certain scenarios - as it uses babel to inspect and rewrite your code before it is ran.
Unlike jest.spyOn
, you can use jest.mock(moduleName, factory, options)
to rewrite entire modules:
jest.mock("../client", function () {
return {
get: jest.fn().mockReturnValueOnce(/* ... */),
post: jest.fn().mockReturnValueOnce(/* ... */),
delete: jest.fn().mockReturnValueOnce(/* ... */),
};
});
When testing you may find yourself manually mocking all exposed methods of a given file. In this scenario it may be easier to rely on Jest's auto-mocking functionality which will mock all exposed functions by default if you do not supply a factory argument:
jest.mock("../foo", function () {
return {
fooMethod: jest.fn(),
barMethod: jest.fn(),
};
});
// Equivalent to the above
// Mock all exposed methods
jest.mock("../foo");
With the automocking functionality, you can still spy on all functions within a module and modify particular behaviors too:
// Although this line appears first,
// the `jest.mock` will run first.
import someModule from "some-module";
// This will spyOn all of your module's
// functions by default. This is hoisted
// and runs first
jest.mock("some-module");
// Your test
beforeEach(function () {
someModule.foo.mockReturnValueOnce("...");
});
Before making use of any of Jest's advanced mocking capabilities, consider whether ajest.spyOn
would suffice instead.