DEV Community

Hauke T.
Hauke T.

Posted on

Jest Recap: Safely Mock Properties and Methods of Global Objects

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 an afterEach() 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);
    });
});
Enter fullscreen mode Exit fullscreen mode

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);
    });
});
Enter fullscreen mode Exit fullscreen mode

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);
    });
});
Enter fullscreen mode Exit fullscreen mode

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();
    });
});
Enter fullscreen mode Exit fullscreen mode

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);
    });
});
Enter fullscreen mode Exit fullscreen mode

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();
    });
});
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)