Unit Testing Chrome Extensions with Jest and Vitest: Complete Guide (2026)
Why Unit Test Chrome Extensions?
Unit tests verify that individual functions and modules work correctly in isolation — without launching a browser. They’re fast (milliseconds per test), reliable (no flaky browser automation), and catch logic bugs early.
The challenge with extension code is the chrome.* API dependency. You can’t call chrome.storage.local.get() in Node.js. The solution: mock the Chrome APIs and use dependency injection.
Setup: Jest
Install
npm install jest --save-devjest.config.js
module.exports = {
testEnvironment: 'node',
setupFiles: ['./tests/setup-chrome-mocks.js'],
testMatch: ['**/tests/**/*.test.js']
};tests/setup-chrome-mocks.js
global.chrome = {
storage: {
local: {
get: jest.fn(),
set: jest.fn(),
remove: jest.fn()
},
sync: {
get: jest.fn(),
set: jest.fn()
},
session: {
get: jest.fn(),
set: jest.fn()
}
},
tabs: {
query: jest.fn(),
create: jest.fn(),
update: jest.fn(),
remove: jest.fn()
},
runtime: {
getManifest: jest.fn(() => ({ version: '1.0.0' })),
sendMessage: jest.fn(),
getURL: jest.fn((path) => `chrome-extension://test-id/${path}`),
lastError: null
},
action: {
setBadgeText: jest.fn(),
setBadgeBackgroundColor: jest.fn()
},
notifications: {
create: jest.fn(),
clear: jest.fn()
},
alarms: {
create: jest.fn(),
get: jest.fn(),
clear: jest.fn()
}
};Setup: Vitest
Install
npm install vitest --save-devvitest.config.js
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
setupFiles: ['./tests/setup-chrome-mocks.js'],
globals: true
}
});The mock file is the same — just replace jest.fn() with vi.fn().
Testing Non-API Code
Functions that don’t use Chrome APIs can be tested directly:
// src/utils.js
function formatTabCount(count) {
if (count === 0) return 'No tabs';
if (count === 1) return '1 tab';
if (count > 99) return '99+';
return `${count} tabs`;
}
function extractDomain(url) {
try {
return new URL(url).hostname.replace('www.', '');
} catch {
return null;
}
}
module.exports = { formatTabCount, extractDomain };// tests/utils.test.js
const { formatTabCount, extractDomain } = require('../src/utils');
describe('formatTabCount', () => {
test('returns "No tabs" for 0', () => {
expect(formatTabCount(0)).toBe('No tabs');
});
test('returns singular for 1', () => {
expect(formatTabCount(1)).toBe('1 tab');
});
test('returns "99+" for counts over 99', () => {
expect(formatTabCount(150)).toBe('99+');
});
test('returns count with "tabs" for normal values', () => {
expect(formatTabCount(42)).toBe('42 tabs');
});
});
describe('extractDomain', () => {
test('extracts domain from URL', () => {
expect(extractDomain('https://www.example.com/path')).toBe('example.com');
});
test('returns null for invalid URL', () => {
expect(extractDomain('not-a-url')).toBeNull();
});
});Testing Chrome API Code
Mocking Storage
// src/settings.js
async function getSettings() {
const result = await chrome.storage.sync.get('settings');
return result.settings || { theme: 'light', notifications: true };
}
async function saveSetting(key, value) {
const settings = await getSettings();
settings[key] = value;
await chrome.storage.sync.set({ settings });
return settings;
}
module.exports = { getSettings, saveSetting };// tests/settings.test.js
const { getSettings, saveSetting } = require('../src/settings');
beforeEach(() => {
jest.clearAllMocks();
});
describe('getSettings', () => {
test('returns stored settings', async () => {
chrome.storage.sync.get.mockResolvedValue({
settings: { theme: 'dark', notifications: false }
});
const settings = await getSettings();
expect(settings.theme).toBe('dark');
expect(settings.notifications).toBe(false);
});
test('returns defaults when no settings stored', async () => {
chrome.storage.sync.get.mockResolvedValue({});
const settings = await getSettings();
expect(settings.theme).toBe('light');
expect(settings.notifications).toBe(true);
});
});
describe('saveSetting', () => {
test('updates a single setting', async () => {
chrome.storage.sync.get.mockResolvedValue({
settings: { theme: 'light', notifications: true }
});
chrome.storage.sync.set.mockResolvedValue(undefined);
const result = await saveSetting('theme', 'dark');
expect(result.theme).toBe('dark');
expect(result.notifications).toBe(true);
expect(chrome.storage.sync.set).toHaveBeenCalledWith({
settings: { theme: 'dark', notifications: true }
});
});
});Testing Tab Operations
// src/tab-manager.js
async function getActiveTabId() {
const [tab] = await chrome.tabs.query({
active: true,
currentWindow: true
});
return tab?.id ?? null;
}
async function closeDuplicateTabs() {
const tabs = await chrome.tabs.query({});
const seen = new Map();
const duplicates = [];
for (const tab of tabs) {
if (seen.has(tab.url)) {
duplicates.push(tab.id);
} else {
seen.set(tab.url, tab.id);
}
}
if (duplicates.length > 0) {
await chrome.tabs.remove(duplicates);
}
return duplicates.length;
}
module.exports = { getActiveTabId, closeDuplicateTabs };// tests/tab-manager.test.js
const { getActiveTabId, closeDuplicateTabs } = require('../src/tab-manager');
describe('getActiveTabId', () => {
test('returns active tab ID', async () => {
chrome.tabs.query.mockResolvedValue([{ id: 42, active: true }]);
const id = await getActiveTabId();
expect(id).toBe(42);
});
test('returns null when no active tab', async () => {
chrome.tabs.query.mockResolvedValue([]);
const id = await getActiveTabId();
expect(id).toBeNull();
});
});
describe('closeDuplicateTabs', () => {
test('closes duplicate tabs', async () => {
chrome.tabs.query.mockResolvedValue([
{ id: 1, url: 'https://example.com' },
{ id: 2, url: 'https://other.com' },
{ id: 3, url: 'https://example.com' }, // duplicate
{ id: 4, url: 'https://example.com' } // duplicate
]);
chrome.tabs.remove.mockResolvedValue(undefined);
const count = await closeDuplicateTabs();
expect(count).toBe(2);
expect(chrome.tabs.remove).toHaveBeenCalledWith([3, 4]);
});
test('does nothing with no duplicates', async () => {
chrome.tabs.query.mockResolvedValue([
{ id: 1, url: 'https://example.com' },
{ id: 2, url: 'https://other.com' }
]);
const count = await closeDuplicateTabs();
expect(count).toBe(0);
expect(chrome.tabs.remove).not.toHaveBeenCalled();
});
});Dependency Injection Pattern
For better testability, inject Chrome APIs rather than using the global:
// src/analytics.js
function createAnalytics(storage = chrome.storage.local) {
return {
async trackEvent(name, data) {
const { events = [] } = await storage.get('events');
events.push({ name, data, timestamp: Date.now() });
await storage.set({ events });
},
async getEvents() {
const { events = [] } = await storage.get('events');
return events;
}
};
}// tests/analytics.test.js
test('tracks events', async () => {
const mockStorage = {
get: jest.fn().mockResolvedValue({ events: [] }),
set: jest.fn().mockResolvedValue(undefined)
};
const analytics = createAnalytics(mockStorage);
await analytics.trackEvent('click', { button: 'save' });
expect(mockStorage.set).toHaveBeenCalledWith({
events: [expect.objectContaining({
name: 'click',
data: { button: 'save' }
})]
});
});Test Organization
tests/
├── setup-chrome-mocks.js # Global Chrome API mocks
├── utils.test.js # Pure utility functions
├── settings.test.js # Storage-based settings
├── tab-manager.test.js # Tab operations
├── analytics.test.js # Analytics module
└── message-handler.test.js # Message passing logicWhat’s Next
Unit tests are the foundation of a reliable extension. Mock Chrome APIs, use dependency injection for testability, and complement unit tests with E2E tests for full coverage.
Build and optimize your extension with ExtensionBooster.
Share this article
Build better extensions with free tools
Icon generator, MV3 converter, review exporter, and more — no signup needed.
Related Articles
I Built the Same Chrome Extension With 5 Different Frameworks. Here's What Actually Happened.
WXT vs Plasmo vs CRXJS vs Extension.js vs Bedframe. Real benchmarks, honest opinions, and the framework with 12K stars that's quietly dying.
5 Best Email Marketing Services to Grow Your Chrome Extension (2026)
Compare the top email marketing platforms for SaaS and Chrome extension developers. MailerLite, Mailchimp, Brevo, ActiveCampaign, and Drip reviewed.
15 Best Practices to Build a Browser Extension That Users Love (2026 Guide)
Master browser extension development in 2026. Manifest V3, security, performance, and UX best practices to build extensions users love.