Chrome Extension Extension Dependency Injection — Best Practices

4 min read

Extension Dependency Injection Patterns

Patterns for writing testable Chrome extensions using dependency injection to decouple from global chrome.* APIs.

Problem

Chrome extension APIs (chrome.storage, chrome.runtime, chrome.alarms) are global singletons. This makes unit testing difficult because you cannot easily mock or replace these globals in your test environment.

Solution: Inject Chrome API Dependencies

Wrap chrome.* APIs in interfaces/classes and inject them as dependencies rather than importing globals directly.

Service Layer Pattern

Create services that accept chrome API objects as constructor parameters:

// storage-service.js
export class StorageService {
  constructor(storageArea = chrome.storage.local) {
    this.storage = storageArea;
  }

  async get(key) {
    return new Promise((resolve) => {
      this.storage.get(key, (result) => resolve(result[key]));
    });
  }

  async set(key, value) {
    return new Promise((resolve) => {
      this.storage.set({ [key]: value }, resolve);
    });
  }
}

Factory Pattern for Production vs Test

Create factories that provide real or mock implementations:

// factories/storage-factory.js
export function createStorageService() {
  return new StorageService(chrome.storage.local);
}

export function createMockStorageService(initialData = {}) {
  const data = { ...initialData };
  return new StorageService({
    get: (keys, cb) => cb(keys.reduce((r, k) => (r[k] = data[k], r), {})),
    set: (obj, cb) => { Object.assign(data, obj); cb(); }
  });
}

Module-Level Injection

Export factory functions that accept dependencies:

// message-service.js
export function createMessageService(runtime = chrome.runtime) {
  return {
    sendMessage(message) {
      return runtime.sendMessage(message);
    },
    onMessage(callback) {
      runtime.onMessage.addListener(callback);
    }
  };
}

Context-Aware Injection

Provide different implementations for background scripts vs content scripts:

// service-registry.js
export function createServiceRegistry(context) {
  const isBackground = context === 'background';
  
  return {
    storage: isBackground 
      ? new StorageService(chrome.storage.local)
      : createContentStorageBridge(),
    messaging: isBackground
      ? new BackgroundMessageService()
      : new ContentScriptMessageService()
  };
}

Injectable Alarm Service

// alarm-service.js
export class AlarmService {
  constructor(alarmsAPI = chrome.alarms) {
    this.alarms = alarmsAPI;
  }

  async schedule(name, delayInMinutes) {
    this.alarms.create(name, { delayInMinutes });
  }

  onAlarm(callback) {
    this.alarms.onAlarm.addListener(callback);
  }
}

TypeScript Interfaces

Define contracts for injectable services:

interface IStorageService {
  get<T>(key: string): Promise<T | undefined>;
  set<T>(key: string, value: T): Promise<void>;
}

interface IMessageService {
  sendMessage(message: object): Promise<void>;
  onMessage(callback: (message: object) => void): void;
}

Benefits

See Also

Part of the Chrome Extension Guide by theluckystrike. Built at zovo.one.