Jest Mock LocalStorage in TypeScript
In modern web development, localStorage is a crucial browser API that allows web applications to store key-value pairs locally in the user's browser. When writing unit tests for TypeScript applications using Jest, mocking localStorage becomes necessary because the testing environment (usually Node.js) doesn't have access to the browser's localStorage. This blog post will guide you through the process of mocking localStorage in a TypeScript project using Jest, covering fundamental concepts, usage methods, common practices, and best practices.
Table of Contents#
Fundamental Concepts#
What is Jest?#
Jest is a JavaScript testing framework developed by Facebook. It provides a simple and powerful way to write unit tests, integration tests, and snapshot tests. Jest has built-in support for mocking, which makes it easy to isolate components and functions during testing.
What is LocalStorage?#
localStorage is a web API that allows web developers to store data locally in the user's browser. The data stored in localStorage persists even after the browser is closed and reopened. It uses a simple key-value storage system, where both keys and values are strings.
Why Mock LocalStorage?#
When writing unit tests for code that interacts with localStorage, we want to isolate the code being tested from the actual browser environment. Mocking localStorage allows us to control the data and behavior of localStorage during testing, ensuring that our tests are deterministic and reliable.
Usage Methods#
Basic Mock Setup#
The simplest way to mock localStorage in Jest is to create a mock object that implements the Storage interface. Here is an example:
// mockLocalStorage.ts
class MockStorage {
private store: { [key: string]: string } = {};
getItem(key: string): string | null {
return this.store[key] || null;
}
setItem(key: string, value: string): void {
this.store[key] = value;
}
removeItem(key: string): void {
delete this.store[key];
}
clear(): void {
this.store = {};
}
}
const mockLocalStorage = new MockStorage();
export default mockLocalStorage;In your test file, you can use this mock localStorage as follows:
// example.test.ts
import mockLocalStorage from './mockLocalStorage';
describe('Test with mocked localStorage', () => {
beforeAll(() => {
Object.defineProperty(window, 'localStorage', {
value: mockLocalStorage,
writable: true
});
});
it('should set and get an item from localStorage', () => {
window.localStorage.setItem('testKey', 'testValue');
const value = window.localStorage.getItem('testKey');
expect(value).toBe('testValue');
});
});Using Jest's jest.fn() for More Control#
You can also use Jest's jest.fn() to create spies on the localStorage methods. This allows you to track how many times a method was called and with what arguments.
// exampleWithSpies.test.ts
describe('Test with localStorage spies', () => {
let originalLocalStorage: Storage;
beforeAll(() => {
originalLocalStorage = window.localStorage;
const mockStorage = {
getItem: jest.fn(),
setItem: jest.fn(),
removeItem: jest.fn(),
clear: jest.fn()
};
Object.defineProperty(window, 'localStorage', {
value: mockStorage,
writable: true
});
});
afterAll(() => {
Object.defineProperty(window, 'localStorage', {
value: originalLocalStorage,
writable: true
});
});
it('should call setItem', () => {
window.localStorage.setItem('key', 'value');
expect(window.localStorage.setItem).toHaveBeenCalledWith('key', 'value');
});
});Common Practices#
Resetting the Mocked State#
It's important to reset the state of the mocked localStorage between tests to ensure that tests are independent of each other. You can do this in the beforeEach or afterEach hooks.
// resetExample.test.ts
import mockLocalStorage from './mockLocalStorage';
describe('Test with mocked localStorage and reset', () => {
beforeAll(() => {
Object.defineProperty(window, 'localStorage', {
value: mockLocalStorage,
writable: true
});
});
beforeEach(() => {
mockLocalStorage.clear();
});
it('should set and get an item', () => {
window.localStorage.setItem('key1', 'value1');
const value = window.localStorage.getItem('key1');
expect(value).toBe('value1');
});
it('should not have previous data', () => {
const value = window.localStorage.getItem('key1');
expect(value).toBeNull();
});
});Testing Error Conditions#
You can also test how your code behaves when localStorage operations fail. For example, you can simulate a QuotaExceededError when calling setItem.
// errorTest.test.ts
import mockLocalStorage from './mockLocalStorage';
describe('Test localStorage error conditions', () => {
beforeAll(() => {
Object.defineProperty(window, 'localStorage', {
value: mockLocalStorage,
writable: true
});
});
it('should handle QuotaExceededError', () => {
const originalSetItem = mockLocalStorage.setItem;
mockLocalStorage.setItem = () => {
throw new DOMException('QuotaExceededError');
};
try {
window.localStorage.setItem('key', 'value');
} catch (error) {
expect(error).toBeInstanceOf(DOMException);
expect((error as DOMException).message).toBe('QuotaExceededError');
}
mockLocalStorage.setItem = originalSetItem;
});
});Best Practices#
Use a Centralized Mocking Function#
Instead of repeating the mocking code in every test file, create a centralized function to set up the mocked localStorage.
// setupLocalStorageMock.ts
import mockLocalStorage from './mockLocalStorage';
export function setupLocalStorageMock() {
Object.defineProperty(window, 'localStorage', {
value: mockLocalStorage,
writable: true
});
}In your test files, you can use it like this:
// bestPractice.test.ts
import { setupLocalStorageMock } from './setupLocalStorageMock';
describe('Test with centralized mock setup', () => {
beforeAll(() => {
setupLocalStorageMock();
});
it('should work with centralized setup', () => {
window.localStorage.setItem('test', 'data');
const value = window.localStorage.getItem('test');
expect(value).toBe('data');
});
});Follow the Single Responsibility Principle#
Each test should test one specific behavior of the code that interacts with localStorage. This makes the tests easier to understand and maintain.
Conclusion#
Mocking localStorage in TypeScript projects using Jest is an essential skill for writing reliable unit tests. By understanding the fundamental concepts, usage methods, common practices, and best practices, you can ensure that your tests are isolated, deterministic, and easy to maintain. Remember to reset the mocked state between tests, test error conditions, and use a centralized mocking function for better code organization.