Mocking the Window Object In Jest Tests

Updated on · 7 min read
Mocking the Window Object In Jest Tests

As developers, we often encounter the window object when writing JavaScript for the web. This global object, representing the browser's window, plays a crucial role in controlling the browser viewport, accessing browser history, setting timers, and much more. However, when it comes to testing our JavaScript code, the window object can be quite challenging to deal with. Its global nature and interaction with the state of the browser can lead to unpredictable behavior and complications in setting up a reliable testing environment.

This is where mocking the window object in Jest comes into play. Mocking allows us to isolate the functionality we're testing and control the environment in which our code is running. It helps us avoid issues arising from side effects or state dependencies of the window object, ultimately making our tests more predictable, easier to debug, and more readable. In this article, we'll explore the best practices for mocking the window object in Jest, using a simple function that retrieves a language code from a page URL as an example.

Understanding the window object

The window object is a global object in client-side JavaScript. It represents the browser's window, and all global JavaScript objects, functions, and variables automatically become its members. It's important to understand that when we talk about the window object, we're talking about a vast range of potential properties and methods.

The window object is used in various ways in JavaScript. Some of the most common uses include:

  • Controlling the browser viewport through methods like window.open(), window.close(), window.resizeTo(), etc.
  • Accessing browser history and controlling navigation with window.history, window.location, etc.
  • Setting timers with window.setTimeout(), window.setInterval(), and their clear counterparts.
  • Accessing and manipulating document elements via the window.document property.
  • Storing data in the user's browser with window.localStorage and window.sessionStorage.

Testing code that uses the window object can be challenging for several reasons:

  • The window object is global, which means it can be accessed and potentially modified from anywhere in your code. This can lead to unpredictable behavior and make it harder to isolate what's being tested.
  • Some properties and methods of the window object have side effects or depend on the state of the browser, which can be hard to replicate in a testing environment.
  • The window object is read-only in many environments, which means you can't easily replace properties or methods for testing.
  • Some properties and methods of the window object, like localStorage, aren't available in all environments, which can cause tests to fail if they're not properly handled.

Why mock the window object?

Mocking the window object in Jest tests is important for a variety of reasons. First, it allows you to isolate the functionality you're testing. By mocking the window object, you can control the environment in which your code is running, ensuring that your tests are not affected by external factors such as the state of the browser or the user's system.

Second, mocking the window object can help you avoid issues that can arise when testing code that interacts with it. For instance, some methods of the window object have side effects that can interfere with your tests, or depend on the state of the browser, which can be hard to replicate in a testing environment. By mocking these methods, you can ensure that they behave exactly as you expect them to during your tests.

Finally, mocking the window object can make your tests more reliable and easier to understand. By explicitly defining the behavior of the window object in your tests, you can make your tests more predictable and easier to debug. You can also make your tests more readable, as it's clear from the test setup what the expected behavior of the window object is.

If you're looking for the best practices when writing tests with React Testing Library, you might find this article helpful: Improving React Testing Library Tests.

Mocking the window object

To demonstrate the best approaches for mocking the window object in tests, we'll use an example of a simple function that retrieves a language code from the URL of a page. We assume that the page URLs will have the format https://example.com/en, where the last part signifies the language code. The example is deliberately very simple, however, it nicely illustrates possible approaches to tests involving global objects.

ts
export const getLanguage = () => { if (!window?.location?.href) { return "en"; } return window.location.href.split("/")[3]; };
ts
export const getLanguage = () => { if (!window?.location?.href) { return "en"; } return window.location.href.split("/")[3]; };

If the window object is not available, such as when the code is running on the server-side, we return a default value of "en". Otherwise, we extract the final segment from the href attribute of the location object.

Opting for dependency injection over mocking

Before we start writing tests for this function, it's worth discussing an alternate approach - dependency injection. This method involves passing the dependencies of the code as arguments. Generally, this is a preferred strategy over creating mocks as it simplifies the testing process. With dependency injection, you can easily pass mock objects or values for your dependencies in your tests.

As an example, we could modify our function to include a url parameter instead of directly accessing window.location.href. In a browser environment, we could then pass window.location.href as an argument when calling the function. During testing, we would simply pass a mock location URL.

ts
export const getLanguage = (url?: string) => { if (!url) { return "en"; } return url.split("/")[3]; };
ts
export const getLanguage = (url?: string) => { if (!url) { return "en"; } return url.split("/")[3]; };

While adding extra parameters to our function may seem cumbersome, the use of dependency injection makes the tests simpler and more robust. It allows us to bypass the complexity that comes with mocking global objects.

However, real-world scenarios often bring more complexity that may not accommodate dependency injection, especially when dealing with third-party components. In such instances, we need to mock the window object.

Using Object.defineProperty to mock the window object

One way to mock the window object in tests is by using the Object.defineProperty method. This JavaScript method enables us to define a new property directly on an object or modify an existing one. Leveraging this, we can define a custom window property that returns a location.href value specifically for testing.

ts
import { getLanguage } from "./getLanguage"; const originalLocation = window; afterEach(() => { Object.defineProperty(globalThis, "window", { value: originalLocation, }); }); describe("useLanguage", () => { it("should test with window", function () { Object.defineProperty(globalThis, "window", { value: { location: { href: "https://example.com/es" } }, writable: true, }); expect(getLanguage()).toBe("es"); }); it("should test with window undefined", function () { Object.defineProperty(globalThis, "window", { value: undefined, }); expect(getLanguage()).toBe("en"); }); });
ts
import { getLanguage } from "./getLanguage"; const originalLocation = window; afterEach(() => { Object.defineProperty(globalThis, "window", { value: originalLocation, }); }); describe("useLanguage", () => { it("should test with window", function () { Object.defineProperty(globalThis, "window", { value: { location: { href: "https://example.com/es" } }, writable: true, }); expect(getLanguage()).toBe("es"); }); it("should test with window undefined", function () { Object.defineProperty(globalThis, "window", { value: undefined, }); expect(getLanguage()).toBe("en"); }); });

Firstly, we save the original window object to a variable so that we can restore it after the tests. It's crucial to reset any modified global objects post-testing to prevent state leakage from one test to another. As we're setting up a different window object for each test in our case, we ensure its restoration after each test run.

We evaluate our function under two conditions - when the window object is defined and when it's undefined, ensuring that it functions as expected in both scenarios. To mock the window object, we define a new window property on the Global object and equip it with a custom location.href. It's essential to add writable: true as we need to redefine it in another test.

Using Jest spy for mocking the window object

An alternate method to mock the window object involves using Jest's mock capabilities.

ts
import { getLanguage } from "./getLanguage"; let windowSpy: jest.SpyInstance; beforeEach(() => { windowSpy = jest.spyOn(globalThis, "window", "get"); }); afterEach(() => { windowSpy.mockRestore(); }); describe("useLanguage", () => { it("should test with window", function () { windowSpy.mockImplementation(() => ({ location: { href: "https://example.com/es", }, })); expect(getLanguage()).toBe("es"); }); it("should test with window undefined", function () { windowSpy.mockImplementation(() => undefined); expect(getLanguage()).toBe("en"); }); });
ts
import { getLanguage } from "./getLanguage"; let windowSpy: jest.SpyInstance; beforeEach(() => { windowSpy = jest.spyOn(globalThis, "window", "get"); }); afterEach(() => { windowSpy.mockRestore(); }); describe("useLanguage", () => { it("should test with window", function () { windowSpy.mockImplementation(() => ({ location: { href: "https://example.com/es", }, })); expect(getLanguage()).toBe("es"); }); it("should test with window undefined", function () { windowSpy.mockImplementation(() => undefined); expect(getLanguage()).toBe("en"); }); });

In this case, we generate an instance of a spy function, which lets us alter the implementation of the window object. We also make sure to restore the mock after each test execution.

Though there is no definitive superior approach between the two, using Jest's mock is marginally preferred due to its robustness and applicability to a broader range of scenarios. Additionally, Jest provides specific functions for restoring and clearing mocks, eliminating the need for manual restoration.

Conclusion

As we've seen, mocking the window object in Jest offers multiple advantages and allows us to create more reliable and robust tests. The use of Object.defineProperty and Jest's mock capabilities provide practical ways of handling this task, with each method having its strengths.

While there isn't a definitive best approach between the two, using Jest's mock is slightly more preferred due to its robustness and versatility. Also, Jest's built-in functions for restoring and clearing mocks eliminate the need for manual restoration, further simplifying the testing process.

Regardless of the method used, the ultimate goal remains the same - to create reliable, robust, and understandable tests. By mocking the window object, we gain more control over our testing environment, allowing us to isolate the functionality we're testing and make our tests easier to debug. With these techniques in your toolbelt, you're well-equipped to tackle testing scenarios involving the window object in JavaScript.

References and resources