Test Setup and Mocking

Test Setup and Teardown

Vitest provides functions that let us set up and clean up our tests:

javascript
	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:

javascript
	// 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:

javascript
	// 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:

  1. We create a simple storage object to hold our data

  2. We create mock versions of setItem and getItem:

    • setItem saves data to our storage object
    • getItem retrieves data from our storage object
  3. We test each function separately

  4. 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):

javascript
	// 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:

javascript
	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:

bash
	npm install -D jsdom @vitest/browser

Create a vitest.config.js file in your project root and add the jsdom config:

javascript
	import { defineConfig } from "vite";
	
	export default defineConfig({
	  test: {
	    environment: "jsdom",
	  },
	});

Now we can write our tests without having to create mocks:

javascript
	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, and afterAll
  • 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