Mocking the Window Object In Jest Tests
Updated on · 7 min read|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
andwindow.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.
tsexport const getLanguage = () => { if (!window?.location?.href) { return "en"; } return window.location.href.split("/")[3]; };
tsexport 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.
tsexport const getLanguage = (url?: string) => { if (!url) { return "en"; } return url.split("/")[3]; };
tsexport 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.
tsimport { 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"); }); });
tsimport { 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.
tsimport { 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"); }); });
tsimport { 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.