Chapter 13: Testing and Debugging Techniques

Haiyue
22min

Chapter 13: Testing and Debugging Techniques

Learning Objectives

  1. Master the use of Chrome extension debugging tools
  2. Understand the application of unit testing and integration testing in extension development
  3. Learn to use developer tools for performance analysis
  4. Master error handling and exception monitoring
  5. Learn to set up and configure testing environments

1. Chrome Extension Debugging Basics

1.1 Using Developer Tools

Chrome extension debugging primarily relies on the browser’s developer tools.

// manifest.json - Development environment configuration
{
  "manifest_version": 3,
  "name": "Debug Extension",
  "version": "1.0",
  "permissions": ["tabs", "storage", "debugger"],
  "background": {
    "service_worker": "background.js",
    "type": "module"
  },
  "content_scripts": [{
    "matches": ["<all_urls>"],
    "js": ["debug-content.js"]
  }],
  "action": {
    "default_popup": "popup.html"
  },
  "devtools_page": "devtools.html"
}
// background.js - Debugger integration
class ExtensionDebugger {
  constructor() {
    this.isDebugMode = true;
    this.logs = [];
    this.setupErrorHandling();
  }

  log(level, message, data = {}) {
    const logEntry = {
      timestamp: new Date().toISOString(),
      level,
      message,
      data,
      stack: new Error().stack
    };

    this.logs.push(logEntry);

    if (this.isDebugMode) {
      console.group(`[${level.toUpperCase()}] ${message}`);
      console.log('Data:', data);
      console.log('Stack:', logEntry.stack);
      console.groupEnd();
    }
  }

  setupErrorHandling() {
    // Global error handling
    self.addEventListener('error', (event) => {
      this.log('error', 'Global Error', {
        message: event.message,
        filename: event.filename,
        lineno: event.lineno,
        colno: event.colno,
        error: event.error
      });
    });

    // Promise rejection handling
    self.addEventListener('unhandledrejection', (event) => {
      this.log('error', 'Unhandled Promise Rejection', {
        reason: event.reason,
        promise: event.promise
      });
    });
  }

  async exportLogs() {
    const blob = new Blob([JSON.stringify(this.logs, null, 2)], {
      type: 'application/json'
    });

    // Save logs to storage
    await chrome.storage.local.set({
      debugLogs: this.logs
    });

    return blob;
  }
}

const debugger = new ExtensionDebugger();

1.2 Advanced Debugging Tool Class

// advanced-debugger.js - Advanced debugging tools
class ChromeExtensionDebugger {
  constructor() {
    this.debugMode = true;
    this.logs = [];
    this.performanceMarks = {};
    this.memoryUsage = {};
  }

  log(level, message, data = {}) {
    const logEntry = {
      timestamp: new Date().toISOString(),
      level,
      message,
      data,
      stack: new Error().stack
    };

    this.logs.push(logEntry);

    if (this.debugMode) {
      console.group(`[${level.toUpperCase()}] ${message}`);
      if (Object.keys(data).length > 0) {
        console.log('Data:', JSON.stringify(data, null, 2));
      }
      console.groupEnd();
    }
  }

  markPerformance(label) {
    this.performanceMarks[label] = Date.now();
    performance.mark(label);
  }

  measurePerformance(startLabel, endLabel) {
    if (this.performanceMarks[startLabel] && this.performanceMarks[endLabel]) {
      const startTime = this.performanceMarks[startLabel];
      const endTime = this.performanceMarks[endLabel];
      const duration = endTime - startTime;

      this.log('performance', `Duration: ${startLabel} to ${endLabel}`, {
        duration_ms: duration,
        start_time: new Date(startTime).toISOString(),
        end_time: new Date(endTime).toISOString()
      });

      return duration;
    }
    return 0;
  }

  trackMemory(component) {
    if (performance.memory) {
      const memoryInfo = performance.memory;

      this.memoryUsage[component] = {
        timestamp: new Date().toISOString(),
        used: memoryInfo.usedJSHeapSize,
        total: memoryInfo.totalJSHeapSize,
        limit: memoryInfo.jsHeapSizeLimit,
        percent: (memoryInfo.usedJSHeapSize / memoryInfo.jsHeapSizeLimit * 100).toFixed(2)
      };

      this.log('memory', `Memory usage for ${component}`, this.memoryUsage[component]);
    } else {
      this.log('warning', 'Memory API not available');
    }
  }

  exportLogs() {
    return {
      timestamp: new Date().toISOString(),
      totalLogs: this.logs.length,
      logs: this.logs,
      memoryUsage: this.memoryUsage,
      performanceMarks: this.performanceMarks
    };
  }
}

// Test tool class
class ChromeExtensionTestCase {
  constructor() {
    this.debugger = new ChromeExtensionDebugger();
    this.chromeMock = this.createChromeMock();
  }

  createChromeMock() {
    return {
      storage: {
        local: {
          set: jest.fn().mockResolvedValue(true),
          get: jest.fn(),
          remove: jest.fn()
        }
      },
      runtime: {
        sendMessage: jest.fn(),
        onMessage: {
          addListener: jest.fn()
        }
      }
    };
  }

  async testBackgroundScript() {
    // Test background script functionality
    const result = await this.simulateStorageOperation();

    if (result) {
      this.debugger.log('info', 'Background script test passed');
      return true;
    } else {
      this.debugger.log('error', 'Background script test failed');
      return false;
    }
  }

  async simulateStorageOperation() {
    try {
      const data = { test_key: 'test_value' };
      this.debugger.log('info', 'Storing data', data);

      await this.chromeMock.storage.local.set(data);
      return true;
    } catch (error) {
      this.debugger.log('error', 'Storage failed', { error: error.message });
      return false;
    }
  }
}

// Usage example
async function runDebuggerExample() {
  const debugger = new ChromeExtensionDebugger();
  debugger.log('info', 'Extension started');
  debugger.markPerformance('start');

  // Simulate some operations
  await new Promise(resolve => setTimeout(resolve, 100));

  debugger.markPerformance('end');
  debugger.measurePerformance('start', 'end');
  debugger.trackMemory('background_script');

  console.log(`\nTotal logs: ${debugger.logs.length}`);

  // Export logs
  const report = debugger.exportLogs();
  console.log('Debug Report:', report);
}

// Run tests
async function runTests() {
  const testCase = new ChromeExtensionTestCase();
  await testCase.testBackgroundScript();
}

2. Unit Testing and Integration Testing

2.1 Jest Testing Framework Integration

// test-setup.js - Test environment configuration
import { jest } from '@jest/globals';

// Mock Chrome API
global.chrome = {
  runtime: {
    sendMessage: jest.fn(),
    onMessage: {
      addListener: jest.fn(),
      removeListener: jest.fn()
    },
    getManifest: jest.fn(() => ({
      name: 'Test Extension',
      version: '1.0'
    }))
  },
  storage: {
    local: {
      get: jest.fn(),
      set: jest.fn(),
      remove: jest.fn()
    },
    sync: {
      get: jest.fn(),
      set: jest.fn(),
      remove: jest.fn()
    }
  },
  tabs: {
    query: jest.fn(),
    create: jest.fn(),
    update: jest.fn()
  }
};

// Mock DOM
global.document = {
  createElement: jest.fn(),
  getElementById: jest.fn(),
  querySelector: jest.fn(),
  querySelectorAll: jest.fn()
};
// storage.test.js - Storage functionality tests
import { StorageManager } from '../src/storage.js';

describe('StorageManager', () => {
  let storageManager;

  beforeEach(() => {
    storageManager = new StorageManager();
    jest.clearAllMocks();
  });

  test('should save data to local storage', async () => {
    const testData = { key: 'value' };
    chrome.storage.local.set.mockResolvedValue();

    await storageManager.saveLocal('testKey', testData);

    expect(chrome.storage.local.set).toHaveBeenCalledWith({
      testKey: testData
    });
  });

  test('should retrieve data from local storage', async () => {
    const testData = { testKey: { key: 'value' } };
    chrome.storage.local.get.mockResolvedValue(testData);

    const result = await storageManager.getLocal('testKey');

    expect(chrome.storage.local.get).toHaveBeenCalledWith('testKey');
    expect(result).toEqual({ key: 'value' });
  });

  test('should handle storage errors', async () => {
    chrome.storage.local.set.mockRejectedValue(new Error('Storage error'));

    await expect(storageManager.saveLocal('testKey', 'value'))
      .rejects.toThrow('Storage error');
  });
});
// message.test.js - Message passing tests
import { MessageHandler } from '../src/message.js';

describe('MessageHandler', () => {
  let messageHandler;

  beforeEach(() => {
    messageHandler = new MessageHandler();
    jest.clearAllMocks();
  });

  test('should send message correctly', async () => {
    chrome.runtime.sendMessage.mockResolvedValue({ success: true });

    const response = await messageHandler.sendMessage('test', { data: 'value' });

    expect(chrome.runtime.sendMessage).toHaveBeenCalledWith({
      action: 'test',
      payload: { data: 'value' }
    });
    expect(response).toEqual({ success: true });
  });

  test('should register message listeners', () => {
    const handler = jest.fn();
    messageHandler.onMessage('test', handler);

    expect(chrome.runtime.onMessage.addListener).toHaveBeenCalled();
  });
});

2.2 End-to-End Testing

// e2e.test.js - End-to-end tests
import puppeteer from 'puppeteer';
import path from 'path';

describe('Extension E2E Tests', () => {
  let browser;
  let page;
  const extensionPath = path.join(__dirname, '../dist');

  beforeAll(async () => {
    browser = await puppeteer.launch({
      headless: false,
      args: [
        `--disable-extensions-except=${extensionPath}`,
        `--load-extension=${extensionPath}`,
        '--no-sandbox'
      ]
    });

    page = await browser.newPage();
  });

  afterAll(async () => {
    await browser.close();
  });

  test('should load extension popup', async () => {
    // Get extension ID
    const targets = await browser.targets();
    const extensionTarget = targets.find(
      target => target.type() === 'service_worker'
    );

    expect(extensionTarget).toBeDefined();

    // Test popup page
    const popupPage = await browser.newPage();
    await popupPage.goto(`chrome-extension://${extensionTarget.url().split('/')[2]}/popup.html`);

    const title = await popupPage.title();
    expect(title).toBe('Extension Popup');
  });

  test('should inject content script', async () => {
    await page.goto('https://example.com');

    // Wait for content script to load
    await page.waitForFunction(() => window.extensionInjected === true);

    const injected = await page.evaluate(() => window.extensionInjected);
    expect(injected).toBe(true);
  });
});

3. Performance Analysis and Optimization

3.1 Performance Monitoring Tools

// performance-monitor.js
class PerformanceMonitor {
  constructor() {
    this.metrics = new Map();
    this.observers = [];
    this.setupObservers();
  }

  setupObservers() {
    // Performance observer
    if (typeof PerformanceObserver !== 'undefined') {
      const observer = new PerformanceObserver((list) => {
        for (const entry of list.getEntries()) {
          this.recordMetric(entry.name, entry.duration, entry.entryType);
        }
      });

      observer.observe({ entryTypes: ['measure', 'navigation'] });
      this.observers.push(observer);
    }

    // Memory usage monitoring
    setInterval(() => {
      if (performance.memory) {
        this.recordMemoryUsage();
      }
    }, 5000);
  }

  recordMetric(name, value, type = 'custom') {
    const timestamp = Date.now();
    if (!this.metrics.has(name)) {
      this.metrics.set(name, []);
    }

    this.metrics.get(name).push({
      value,
      type,
      timestamp
    });

    // Keep last 100 records
    const entries = this.metrics.get(name);
    if (entries.length > 100) {
      entries.splice(0, entries.length - 100);
    }
  }

  recordMemoryUsage() {
    const memory = performance.memory;
    this.recordMetric('memory.used', memory.usedJSHeapSize, 'memory');
    this.recordMetric('memory.total', memory.totalJSHeapSize, 'memory');
    this.recordMetric('memory.limit', memory.jsHeapSizeLimit, 'memory');
  }

  mark(name) {
    performance.mark(name);
  }

  measure(name, startMark, endMark) {
    performance.measure(name, startMark, endMark);
  }

  getMetrics() {
    const result = {};
    for (const [name, values] of this.metrics) {
      result[name] = {
        count: values.length,
        average: values.reduce((sum, v) => sum + v.value, 0) / values.length,
        min: Math.min(...values.map(v => v.value)),
        max: Math.max(...values.map(v => v.value)),
        latest: values[values.length - 1]
      };
    }
    return result;
  }

  generateReport() {
    const metrics = this.getMetrics();
    const report = {
      timestamp: new Date().toISOString(),
      metrics,
      recommendations: this.generateRecommendations(metrics)
    };

    console.table(metrics);
    return report;
  }

  generateRecommendations(metrics) {
    const recommendations = [];

    // Memory usage recommendations
    if (metrics['memory.used']?.average > 50 * 1024 * 1024) {
      recommendations.push({
        type: 'memory',
        severity: 'warning',
        message: 'High memory usage detected. Consider optimizing data structures.'
      });
    }

    // Performance recommendations
    for (const [name, data] of Object.entries(metrics)) {
      if (data.average > 100 && name !== 'memory.limit') {
        recommendations.push({
          type: 'performance',
          severity: 'info',
          message: `${name} has high average duration (${data.average.toFixed(2)}ms)`
        });
      }
    }

    return recommendations;
  }
}

// Usage example
const monitor = new PerformanceMonitor();

// Add markers before and after critical operations
monitor.mark('operation-start');
// Execute operation...
monitor.mark('operation-end');
monitor.measure('operation-duration', 'operation-start', 'operation-end');

4. Error Handling and Exception Monitoring

4.1 Error Monitoring System

// error-monitor.js
class ErrorMonitor {
  constructor() {
    this.errors = [];
    this.maxErrors = 1000;
    this.listeners = new Set();
    this.setupErrorHandlers();
  }

  setupErrorHandlers() {
    // JavaScript errors
    self.addEventListener('error', (event) => {
      this.captureError({
        type: 'javascript',
        message: event.message,
        filename: event.filename,
        lineno: event.lineno,
        colno: event.colno,
        stack: event.error?.stack,
        timestamp: Date.now()
      });
    });

    // Promise rejections
    self.addEventListener('unhandledrejection', (event) => {
      this.captureError({
        type: 'promise',
        message: event.reason?.message || 'Unhandled Promise Rejection',
        reason: event.reason,
        stack: event.reason?.stack,
        timestamp: Date.now()
      });
    });

    // Chrome extension specific errors
    if (chrome?.runtime?.lastError) {
      this.captureError({
        type: 'chrome',
        message: chrome.runtime.lastError.message,
        timestamp: Date.now()
      });
    }
  }

  captureError(error) {
    // Add context information
    const enrichedError = {
      ...error,
      id: this.generateErrorId(),
      url: location?.href || 'unknown',
      userAgent: navigator?.userAgent || 'unknown',
      manifest: chrome?.runtime?.getManifest() || {}
    };

    this.errors.push(enrichedError);

    // Limit error count
    if (this.errors.length > this.maxErrors) {
      this.errors.shift();
    }

    // Notify listeners
    this.notifyListeners(enrichedError);

    // Log to console
    console.error('[ErrorMonitor]', enrichedError);
  }

  generateErrorId() {
    return Date.now().toString(36) + Math.random().toString(36).substr(2);
  }

  notifyListeners(error) {
    this.listeners.forEach(listener => {
      try {
        listener(error);
      } catch (e) {
        console.error('Error in error listener:', e);
      }
    });
  }

  onError(listener) {
    this.listeners.add(listener);
    return () => this.listeners.delete(listener);
  }

  getErrors(filter = {}) {
    let filtered = this.errors;

    if (filter.type) {
      filtered = filtered.filter(e => e.type === filter.type);
    }

    if (filter.since) {
      filtered = filtered.filter(e => e.timestamp >= filter.since);
    }

    if (filter.message) {
      filtered = filtered.filter(e =>
        e.message.toLowerCase().includes(filter.message.toLowerCase())
      );
    }

    return filtered.sort((a, b) => b.timestamp - a.timestamp);
  }

  generateErrorReport() {
    const now = Date.now();
    const lastHour = now - 3600000;
    const recentErrors = this.getErrors({ since: lastHour });

    const report = {
      timestamp: new Date().toISOString(),
      totalErrors: this.errors.length,
      recentErrors: recentErrors.length,
      errorTypes: this.groupBy(this.errors, 'type'),
      topErrors: this.getTopErrors(5),
      timeline: this.generateTimeline()
    };

    return report;
  }

  groupBy(array, key) {
    return array.reduce((groups, item) => {
      const group = item[key] || 'unknown';
      groups[group] = (groups[group] || 0) + 1;
      return groups;
    }, {});
  }

  getTopErrors(count = 5) {
    const errorGroups = this.groupBy(this.errors, 'message');
    return Object.entries(errorGroups)
      .sort(([,a], [,b]) => b - a)
      .slice(0, count)
      .map(([message, count]) => ({ message, count }));
  }

  generateTimeline() {
    const hours = 24;
    const timeline = new Array(hours).fill(0);
    const now = Date.now();

    this.errors.forEach(error => {
      const hoursAgo = Math.floor((now - error.timestamp) / 3600000);
      if (hoursAgo < hours) {
        timeline[hours - 1 - hoursAgo]++;
      }
    });

    return timeline;
  }

  async exportErrors() {
    const report = this.generateErrorReport();
    const blob = new Blob([JSON.stringify(report, null, 2)], {
      type: 'application/json'
    });

    // Save to storage
    await chrome.storage.local.set({
      errorReport: report,
      lastReportTime: Date.now()
    });

    return blob;
  }
}

// Global error monitor instance
const errorMonitor = new ErrorMonitor();

// Error notification example
errorMonitor.onError((error) => {
  if (error.type === 'javascript' && error.message.includes('critical')) {
    // Send urgent notification
    chrome.notifications?.create({
      type: 'basic',
      iconUrl: 'icon.png',
      title: 'Extension Error',
      message: 'A critical error occurred in the extension'
    });
  }
});
Debugging Techniques
  1. Use console.group() to organize log output
  2. Leverage performance.mark() and performance.measure() to measure performance
  3. Enable detailed logging in development environment
  4. Use Chrome DevTools Network and Performance panels
Important Notes
  • Turn off debug logging in production to avoid performance impact
  • Error monitoring should not affect normal functionality
  • Protect user privacy, do not collect sensitive information

5. Test Automation and CI/CD Integration

5.1 GitHub Actions Configuration

# .github/workflows/test.yml
name: Chrome Extension Tests

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v3

    - name: Setup Node.js
      uses: actions/setup-node@v3
      with:
        node-version: '18'
        cache: 'npm'

    - name: Install dependencies
      run: npm ci

    - name: Run linting
      run: npm run lint

    - name: Run unit tests
      run: npm test -- --coverage

    - name: Build extension
      run: npm run build

    - name: Run E2E tests
      run: npm run test:e2e

    - name: Upload coverage reports
      uses: codecov/codecov-action@v3
      with:
        file: ./coverage/lcov.info

    - name: Package extension
      if: github.ref == 'refs/heads/main'
      run: npm run package

    - name: Upload artifacts
      uses: actions/upload-artifact@v3
      with:
        name: extension-package
        path: dist/*.zip

Learning Summary

This chapter covered testing and debugging techniques for Chrome extensions:

  1. Debugging Tools: Mastered the application of Chrome developer tools in extension debugging
  2. Testing Framework Integration: Learned Jest unit testing and Puppeteer end-to-end testing
  3. Performance Monitoring: Implemented performance metrics collection and analysis system
  4. Error Handling: Established a complete error monitoring and reporting mechanism
  5. Automated Testing: Configured CI/CD processes to ensure code quality

These skills will help developers build more stable, high-performance Chrome extension applications.

Mermaid Flow Diagram

🔄 正在渲染 Mermaid 图表...

Categories