When writing unit tests for business logic that uses the Window object, we need to mock it in order to fake certain behaviors. Since our use of the window object corresponds frequently to manipulating the browser.
In this tutorial, we will learn how to mock the Window object, and develop a few examples in order to understand this better.
This tutorial is Framework agnostic. Let’s get started, shall we?
Prerequisites
Set the Vitest environment as browser-based. Use jsdom or happy-dom. you can do that inside vite.config.js (create it if it doesn’t exist):
import { defineConfig } from 'vite'
export default defineConfig({
test: {
environment: 'happy-dom',
},
})
This will provide us with a window object in our test environment.
How to Mock Location Reload
We usually access the window location to reload or redirect. Let’s imagine we have two functions that do these operations:
export function reloadWindow() {
window.location.reload()
}
export function changeWindowLocation() {
window.location = 'https://www.google.com'
}
Let’s then implement their unit tests. First, write the spec for the reload function:
import { describe } from "vitest";
describe('Window Functions', () => {
describe('reloadWindow', () => {
test('reloads the window', () => {})
})
})
Then, implement the test by calling the function and expecting that window.reload
was called:
import { describe, expect, test } from 'vitest'
import { reloadWindow } from './window-functions'
describe('Window Functions', () => {
describe('reloadWindow', () => {
test('reloads the window', () => {
reloadWindow()
expect(window.location.reload).toHaveBeenCalled()
})
})
})
At this point, the test will fail because the reload method is not mocked, hence, we can’t use it with toHaveBeenCalled
.
To fix this, spy on it at the beginning of the outer describe block:
import { describe, expect, test, vi } from 'vitest'
import { reloadWindow } from './window-functions'
describe('Window Functions', () => {
vi.spyOn(window.location, 'reload')
describe('reloadWindow', () => {
test('reloads the window', () => {
reloadWindow()
expect(window.location.reload).toHaveBeenCalled()
})
})
})
Since we are not altering the implementation of the method, we can just use vi.spyOn
instead of vi.fn
. so spying instead of mocking…
How to Assert the Current Location
Let’s do the same thing for changeWindowLocation
function, First, we start with the spec:
import { describe, expect, test, vi } from 'vitest'
import { changeWindowLocation, reloadWindow } from './window-functions'
describe('Window Functions', () => {
vi.spyOn(window.location, 'reload')
describe('reloadWindow', () => {
test('reloads the window', () => {
reloadWindow()
expect(window.location.reload).toHaveBeenCalled()
})
})
describe('changeWindowLocation', () => {
test('changes location to https://www.google.com', () => {})
})
})
Then we assert that after calling the function the window.location
value changes to the expected one:
import { describe, expect, test, vi } from 'vitest'
import { changeWindowLocation, reloadWindow } from './window-functions'
describe('Window Functions', () => {
vi.spyOn(window.location, 'reload')
describe('reloadWindow', () => {
test('reloads the window', () => {
reloadWindow()
expect(window.location.reload).toHaveBeenCalled()
})
})
describe('changeWindowLocation', () => {
test('changes location to https://www.google.com', () => {
changeWindowLocation()
expect(window.location).toBe('https://www.google.com')
})
})
})
If we run the tests, they should work, as you can see in the figure below.
We didn’t have to mock anything this time, life is beautiful this way isn’t it? well, I am afraid to let you know that we have an issue actually, allow me to demonstrate!
If we add a new test that checks the window location, what do you think the result would be? let’s find out:
import { describe, expect, test, vi } from 'vitest'
import { changeWindowLocation, reloadWindow } from './window-functions'
describe('Window Functions', () => {
vi.spyOn(window.location, 'reload')
describe('reloadWindow', () => {
test('reloads the window', () => {
reloadWindow()
expect(window.location.reload).toHaveBeenCalled()
})
})
describe('changeWindowLocation', () => {
test('changes location to https://www.google.com', () => {
changeWindowLocation()
expect(window.location).toBe('https://www.google.com')
})
})
test('window location should not be google.com here?', () => {
expect(window.location).not.toBe('https://www.google.com')
})
})
and the result is…
The problem we have is the window location is still carrying the same state between different tests, that’s a big no-no. Each test should have a fresh location object so that we don’t fall into false positive conditions.
To fix this, store the original “fresh” window.location
in a constant, then before each test reset the current window.location
to that “fresh” version:
import { beforeEach, describe, expect, test, vi } from 'vitest'
import { changeWindowLocation, reloadWindow } from './window-functions'
describe('Window Functions', () => {
const originalWindowLocation = window.location
vi.spyOn(window.location, 'reload')
beforeEach(() => {
window.location = originalWindowLocation
})
describe('reloadWindow', () => {
test('reloads the window', () => {
reloadWindow()
expect(window.location.reload).toHaveBeenCalled()
})
})
describe('changeWindowLocation', () => {
test('changes location to https://www.google.com', () => {
changeWindowLocation()
expect(window.location).toBe('https://www.google.com')
})
})
test('window location should not be google.com here?', () => {
expect(window.location).not.toBe('https://www.google.com')
})
})
Now life is beautiful again:
Closing Thoughts
You have now mastered the art of testing the window’s location object.
You can find the code on GitHub. Also, this tutorial is part of a series that goes through mocking the commonly used Web APIs and Packages, you can find the others here. Cheers!