Test Setup and Teardown
Vitest provides functions that let us set up and clean up our tests:
describe("example", () => {
beforeEach(() => {
// Runs before each test
console.log("Setting up test...");
});
afterEach(() => {
// Runs after each test
console.log("Cleaning up test...");
});
beforeAll(() => {
// Runs once before all tests
console.log("Setting up all tests...");
});
afterAll(() => {
// Runs once after all tests
console.log("Cleaning up all tests...");
});
it("test 1", () => {
console.log("Running test 1");
});
it("test 2", () => {
console.log("Running test 2");
});
});
We use these functions when our tests need similar setup.
We will see beforeEach
in use shortly.
What is Mocking and Why Do We Need It?
When we write tests for our code, we often need to test parts that use browser features like:
- localStorage for saving data
- DOM elements for updating the page
- geolocation
But there’s a problem - our tests run in Node.js which doesn’t have these browser features.
This is where mocking comes in. Mocking means creating fake versions of things our code needs. Instead of using the real localStorage in the browser, for example, we create a simple fake version just for our tests.
Mocking localStorage
Let’s look at an example of how we might mock localStorage
for our tests. We’ll use these functions from the storage utility file in the repo:
// js/utils/storage.js
const tokenKey = "token";
export function saveToken(token) {
saveToStorage(tokenKey, token);
}
export function getToken() {
return getFromStorage(tokenKey);
}
function saveToStorage(key, value) {
localStorage.setItem(key, JSON.stringify(value));
}
function getFromStorage(key) {
const value = localStorage.getItem(key);
return value ? JSON.parse(value) : null;
}
To test these functions, we need to mock the two localStorage methods they use - setItem
and getItem
:
// js/utils/storage.test.js
import { expect, describe, it, beforeEach } from "vitest";
import { saveToken, getToken } from "./storage";
describe("Storage functions", () => {
beforeEach(() => {
// Create a simple object to store our data
const storage = {};
// Create mock versions of the localStorage methods we need
global.localStorage = {
setItem: (key, value) => (storage[key] = value),
getItem: (key) => storage[key],
};
});
describe("saveToken", () => {
it("saves the token to storage", () => {
const testToken = "test-token";
saveToken(testToken);
expect(localStorage.getItem("token")).toBe(JSON.stringify(testToken));
});
});
describe("getToken", () => {
it("retrieves the token from storage", () => {
// Set up - directly save a token to localStorage
localStorage.setItem("token", JSON.stringify("test-token"));
const retrievedToken = getToken();
expect(retrievedToken).toBe("test-token");
});
it("returns null when no token exists", () => {
const token = getToken();
// Will return null because beforeEach gives us a fresh empty storage
expect(token).toBeNull();
});
});
});
Let’s break down what’s happening:
We create a simple storage object to hold our data
We create mock versions of setItem and getItem:
setItem
saves data to our storage objectgetItem
retrieves data from our storage object
We test each function separately
Each test starts with a fresh storage object
Testing Practices
Let’s look at what makes a good test.
Here’s an example of testing implementation details (which we want to avoid):
// Testing implementation details (avoid this):
describe("Storage functions", () => {
beforeEach(() => {
global.localStorage = {
getItem: vi.fn(),
setItem: vi.fn(),
};
});
it("calls localStorage.setItem correctly", () => {
const testToken = "test-token";
saveToken(testToken);
expect(localStorage.setItem).toHaveBeenCalledWith(
"token",
JSON.stringify(testToken)
);
});
});
Here’s how to test the actual behavior instead:
describe("Storage functions", () => {
beforeEach(() => {
const storage = {};
global.localStorage = {
setItem: (key, value) => (storage[key] = value),
getItem: (key) => storage[key],
};
});
describe("saveToken", () => {
it("saves the token to storage", () => {
const testToken = "test-token";
saveToken(testToken);
expect(localStorage.getItem("token")).toBe(JSON.stringify(testToken));
});
});
});
Why is the second example better?
- Tests what users care about - can we save data?
- Won’t break if we change how we store the token
- Tests the actual behavior, not the implementation
The first example would break if we:
- Switched to sessionStorage
- Changed how we format the data
The second example would keep passing as long as the save functionality works.
Using jsdom with Vitest
While manually mocking localStorage
works, jsdom provides a more complete simulation of a browser environment in Node.js. This includes localStorage
and many other browser APIs, making our tests more realistic and reducing the need for manual mocking.
Using jsdom with Vitest
So far, we’ve learned how to manually mock localStorage
by creating our own fake version. While this works, there’s an easier way: using jsdom
.
jsdom is like a fake browser that runs in Node.js. It provides ready-made versions of browser features like:
- localStorage
- DOM elements (like buttons and forms)
- window object
- And many other browser features
This means instead of writing our own mocks, we can use jsdom’s pre-built browser environment. This is especially helpful when we need to test code that uses multiple browser features.
First, install the necessary packages:
npm install -D jsdom @vitest/browser
Create a vitest.config.js
file in your project root and add the jsdom config:
import { defineConfig } from "vite";
export default defineConfig({
test: {
environment: "jsdom",
},
});
Now we can write our tests without having to create mocks:
import { expect, describe, it, beforeEach } from "vitest";
import { saveToken, getToken } from "./storage";
describe("Storage functions", () => {
beforeEach(() => {
// Clear localStorage before each test
localStorage.clear();
});
describe("saveToken", () => {
it("saves the token to storage", () => {
const testToken = "test-token";
saveToken(testToken);
expect(localStorage.getItem("token")).toBe(JSON.stringify(testToken));
});
});
describe("getToken", () => {
it("retrieves the token from storage", () => {
localStorage.setItem("token", JSON.stringify("test-token"));
const retrievedToken = getToken();
expect(retrievedToken).toBe("test-token");
});
// Will return null because beforeEach gives us a fresh empty storage
it("returns null when no token exists", () => {
const token = getToken();
expect(token).toBeNull();
});
});
});
The tests are simpler now because jsdom provides localStorage for us. We just need to:
- Clear localStorage before each test
- Write our tests as if we were in a real browser
What We Learned
In this lesson, we:
- Learned about test setup and teardown using
beforeEach
,afterEach
,beforeAll
, andafterAll
- Understood what mocking is and why we need it for browser features in Node.js
- Created manual mocks for localStorage to test storage functions
- Learned how to write tests that focus on behavior instead of implementation details
- Discovered how to use jsdom to get a complete browser environment for testing
Remember:
- Use setup and teardown to avoid repeating code in your tests
- Mock browser features that aren’t available in Node.js
- Test what your code does (behavior), not how it does it (implementation)
- Use jsdom when you need to test code that uses many browser features