Jest Recap: Safely Mock Properties and Methods of Global Objects
Hauke T.
Posted on July 18, 2024
TL;DR:
- You want to avoid that overridden/mocked properties/methods affect other tests.
- For local objects (created and owned by this test) you can (and should) use
-
localObject.theAnswer = 42
and -
localObject.calcTheAnswer = jest.fn(() => 42)
.
-
- For global objects you should use
-
jest.replaceProperty(globalObject, "theAnswer", 42)
and -
jest.spyOn(globalObject, "calcTheAnswer").mockReturnValue(42)
.
-
- Make sure
jest.restoreAllMocks()
is called in anafterEach()
hook.
What?
In a perfect world codebase there is no need to manipulate global objects, but the world codebases are messy - and so is testing.
What you want to avoid at all costs, is for one test to affect the other. Tests should be meaningful regardless of their order or if some tests are skipped.
Mocking Properties
A naive approach to mock values is to just set the properties to whatever value you need in your test.
This is fine as long as you change values in local objects owned (created by) this specific test:
describe("override properties of local objects", () => {
it("works and is harmless", () => {
const myArray = [1];
myArray.length = 0;
expect(myArray).toHaveLength(0);
});
it("does not affect the next test", () => {
const myArray = [1];
expect(myArray).toHaveLength(1);
});
});
If you do that for global objects, it gets messy:
describe("don't override properties of global objects", () => {
it("works before the property is overridden", () => {
expect(window.innerWidth).toBeGreaterThan(0);
});
it("works, but is evil", () => {
window.innerWidth = 0;
expect(window.innerWidth).toBe(0);
});
it("fails in the test after the property was overridden", () => {
expect(() => {
expect(window.innerWidth).toBeGreaterThan(0); // <-- ERROR: expect(received).toBeGreaterThan(expected)
}).toThrow(Error);
});
});
This is what jest.replaceProperty()
was made for:
describe("use jest.replaceProperty() to override properties of global objects", () => {
afterEach(() => {
jest.restoreAllMocks();
});
it("works before the property is overridden", () => {
expect(window.innerWidth).toBeGreaterThan(0);
});
it("works and is harmless", () => {
jest.replaceProperty(window, "innerWidth", 0);
expect(window.innerWidth).toBe(0);
});
it("does not affect the next test", () => {
expect(window.innerWidth).toBeGreaterThan(0);
});
});
Mocking Methods
Methods can be mocked similar to properties.
describe("override methods of local objects using jest.fn()", () => {
it("works and is harmless", () => {
const mySet = new Set([1]);
mySet.has = jest.fn().mockReturnValue(false);
expect(mySet.has(1)).toBeFalsy();
});
it("does not affect the next test", () => {
const mySet = new Set([1]);
expect(mySet.has(1)).toBeTruthy();
});
});
If you use myObject.someFunction = jest.fn()
on global objects, your tests may depend on each other and loose their meaning:
describe("don't override methods of global objects using jest.fn()", () => {
it("works before the method is overridden", () => {
expect(document.getElementById("foo")).toBeNull();
});
it("works, but is evil", () => {
const el = document.createElement("div");
document.getElementById = jest.fn().mockReturnValue(el);
expect(document.getElementById("foo")).toBe(el);
});
it("fails in the test after the property was overridden", () => {
expect(() => {
expect(document.getElementById("foo")).toBeNull(); // <-- ERROR: expect(received).toBeNull()
}).toThrow(Error);
});
});
How should we mock methods in global objects? That is what jest.spyOn()
is good for:
describe("use jest.spyOn() to override methods of global objects", () => {
afterEach(() => {
jest.restoreAllMocks();
});
it("works before the method is overridden", () => {
expect(document.getElementById("foo")).toBeNull();
});
it("works and is harmless", () => {
const el = document.createElement("div");
jest.spyOn(document, "getElementById").mockReturnValue(el);
expect(document.getElementById("foo")).toBe(el);
});
it("does not affect the next test", () => {
expect(document.getElementById("foo")).toBeNull();
});
});
You Must Clean up
If you want to be sure all tests find the system in the same (fresh, clean) state, you need to restore the state of mocks after each test.
The simplest solution is to set the restoreMocks
configuration property.
The most straight forward option is to call jest.restoreAllMocks()
in the afterEach()
How to Mock Something for all Tests
Sometimes you want to Mock things for all tests in a file. If you use jest.spyOn()
and jest.replaceProperty()
on the top-level or in a describe()
block, all Mocks will be reset after the first test is executed.
On the top-level you can safely override properties and methods, without jest.spyOn()
and jest.replaceProperty()
.
If you want to Mock things only for a describe()
block, you need to make these calls in a beforeEach()
hook.
Posted on July 18, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.