Chapter 13: Testing and Debugging Techniques
Haiyue
22min
Chapter 13: Testing and Debugging Techniques
Learning Objectives
- Master the use of Chrome extension debugging tools
- Understand the application of unit testing and integration testing in extension development
- Learn to use developer tools for performance analysis
- Master error handling and exception monitoring
- 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
- Use console.group() to organize log output
- Leverage performance.mark() and performance.measure() to measure performance
- Enable detailed logging in development environment
- 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:
- Debugging Tools: Mastered the application of Chrome developer tools in extension debugging
- Testing Framework Integration: Learned Jest unit testing and Puppeteer end-to-end testing
- Performance Monitoring: Implemented performance metrics collection and analysis system
- Error Handling: Established a complete error monitoring and reporting mechanism
- 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 图表...