霍格沃兹测试开发学社

《Python测试开发进阶训练营》(随到随学!)
2023年第2期《Python全栈开发与自动化测试班》(开班在即)
报名联系weixin/qq:2314507862

Playwright错误处理与重试机制实现

关注 霍格沃兹测试学院公众号,回复「资料」, 领取人工智能测试开发技术合集

在实际的自动化测试和网络爬虫开发中,稳定性是衡量脚本质量的重要指标。即使编写了最完善的Playwright脚本,也不可避免地会遇到各种运行时错误:元素加载延迟、网络波动、页面响应超时等。本文将分享如何构建健壮的Playwright错误处理与重试机制。

为什么需要错误处理机制?
我曾遇到过这样的场景:一个精心编写的爬虫脚本在本地运行完美,但放到服务器上却频繁失败。调查后发现,服务器与目标网站之间的网络延迟较高,导致元素加载时间超出预期。这就是缺乏错误处理机制的典型表现。

基础错误处理策略

  1. 智能等待替代硬性等待
    新手常犯的错误是使用page.waitForTimeout(5000)这样的固定等待。更好的做法是使用Playwright内置的智能等待方法:

// 不推荐 - 硬性等待
await page.waitForTimeout(5000);
await page.click('#submit');

// 推荐 - 智能等待
await page.waitForSelector('#submit', { state: 'visible' });
await page.click('#submit');
2. 异常捕获基础
最简单的错误处理是try-catch块:

async function safeClick(page, selector) {
try {
await page.click(selector);
return true;
} catch (error) {
console.warn(点击元素 ${selector} 失败:, error.message);
return false;
}
}
实现重试机制
基础重试函数
下面是一个通用的重试函数,可以包装任何可能失败的操作:

async function withRetry(
operation,
maxAttempts = 3,
delay = 1000,
backoffFactor = 2
) {
let lastError;

for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
returnawait operation();
} catch (error) {
lastError = error;
console.log(尝试 ${attempt}/${maxAttempts} 失败:, error.message);

  if (attempt === maxAttempts) break;
  
  const waitTime = delay * Math.pow(backoffFactor, attempt - 1);
  console.log(`等待 ${waitTime}ms 后重试...`);
  awaitnewPromise(resolve => setTimeout(resolve, waitTime));
}

}

throw lastError;
}
页面操作重试包装器
针对常见的页面操作,我们可以创建专门的重试包装器:

class RetryablePage {
constructor(page) {
this.page = page;
}

async clickWithRetry(selector, options = {}) {
const maxAttempts = options.maxAttempts || 3;

for (let attempt = 1; attempt <= maxAttempts; attempt++) {
  try {
    // 确保元素可见且可点击
    awaitthis.page.waitForSelector(selector, {
      state: 'visible',
      timeout: 10000
    });
    
    awaitthis.page.click(selector);
    return;
  } catch (error) {
    console.log(`点击 ${selector} 失败 (尝试 ${attempt}/${maxAttempts}):`, error.message);
    
    if (attempt === maxAttempts) {
      thrownewError(`多次尝试点击 ${selector} 均失败: ${error.message}`);
    }
    
    // 等待时间递增
    awaitthis.page.waitForTimeout(1000 * attempt);
  }
}

}

async navigateWithRetry(url, options = {}) {
const maxAttempts = options.maxAttempts || 3;

for (let attempt = 1; attempt <= maxAttempts; attempt++) {
  try {
    const response = awaitthis.page.goto(url, {
      waitUntil: 'networkidle',
      timeout: 30000
    });
    
    if (response && !response.ok()) {
      thrownewError(`HTTP ${response.status()}: ${response.statusText()}`);
    }
    
    return response;
  } catch (error) {
    console.log(`访问 ${url} 失败 (尝试 ${attempt}/${maxAttempts}):`, error.message);
    
    if (attempt === maxAttempts) {
      throw error;
    }
    
    // 如果是网络错误,尝试刷新页面
    if (error.message.includes('net::') || error.message.includes('Navigation')) {
      awaitthis.page.reload();
    }
    
    awaitthis.page.waitForTimeout(2000 * attempt);
  }
}

}
}
Playwright Test中的重试机制
如果你使用Playwright Test框架,它内置了重试功能:

// playwright.config.js
module.exports = {
// 全局重试配置
retries: process.env.CI ? 2 : 1,

use: {
// 操作失败时的截图配置
screenshot: 'only-on-failure',

// 视频录制配置
video: 'retain-on-failure',

},

// 项目级别的重试配置
projects: [
{
name: 'chromium',
retries: 2,
use: { browserName: 'chromium' },
},
],
};
自定义测试重试逻辑
对于需要特殊处理的重试场景,可以在测试内部实现:

import { test, expect } from'@playwright/test';

test('重要的支付流程测试', async ({ page }) => {
let paymentSuccessful = false;

for (let attempt = 1; attempt <= 3; attempt++) {
try {
// 执行支付流程
await page.goto('/payment');
await page.fill('#card-number', '4111111111111111');
await page.click('#pay-now');

  // 验证支付成功
  await expect(page.locator('.success-message')).toBeVisible({
    timeout: 10000
  });
  
  paymentSuccessful = true;
  break;
} catch (error) {
  console.log(`支付测试尝试 ${attempt} 失败:`, error.message);
  
  if (attempt === 3) {
    throw error;
  }
  
  // 清理状态,准备重试
  await page.goto('/cart');
  await page.waitForTimeout(2000 * attempt);
}

}

expect(paymentSuccessful).toBeTruthy();
});
高级错误处理策略

  1. 错误分类与不同处理策略
    class ErrorHandler {
    static shouldRetry(error) {
    const retryableErrors = [
    'TimeoutError',
    'NetworkError',
    'net::',
    'Target closed',
    'Element not found'
    ];

    const errorMessage = error.toString();

    return retryableErrors.some(retryableError =>
    errorMessage.includes(retryableError)
    );
    }

static classifyError(error) {
const message = error.toString();

if (message.includes('Timeout')) {
  return'TIMEOUT';
} elseif (message.includes('net::')) {
  return'NETWORK';
} elseif (message.includes('not found') || message.includes('not visible')) {
  return'ELEMENT_NOT_FOUND';
} else {
  return'UNKNOWN';
}

}
}

asyncfunction resilientOperation(operation) {
const maxAttempts = 3;
let lastError;

for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
returnawait operation();
} catch (error) {
lastError = error;

  if (!ErrorHandler.shouldRetry(error) || attempt === maxAttempts) {
    break;
  }
  
  const errorType = ErrorHandler.classifyError(error);
  const delay = this.calculateDelay(attempt, errorType);
  
  console.log(`[${errorType}] 尝试 ${attempt} 失败,${delay}ms 后重试`);
  awaitnewPromise(resolve => setTimeout(resolve, delay));
}

}

throw lastError;
}
2. 上下文恢复与状态重置
某些错误需要重置浏览器上下文:

async function withContextRecovery(browser, operation) {
let context;
let page;

for (let attempt = 1; attempt <= 3; attempt++) {
try {
if (!context || context._closed) {
context = await browser.newContext();
page = await context.newPage();
}

  returnawait operation(page);
} catch (error) {
  console.log(`操作失败,尝试 ${attempt}/3:`, error.message);
  
  if (attempt === 3) {
    throw error;
  }
  
  // 清理旧上下文
  if (context && !context._closed) {
    await context.close();
  }
  
  // 短暂等待后继续
  awaitnewPromise(resolve => setTimeout(resolve, 1000 * attempt));
}

}
}
Playwright mcp技术学习交流群
伙伴们,对AI测试、大模型评测、质量保障感兴趣吗?我们建了一个 「Playwright mcp技术学习交流群」,专门用来探讨相关技术、分享资料、互通有无。无论你是正在实践还是好奇探索,都欢迎扫码加入,一起抱团成长!期待与你交流!👇

image

实战:完整的爬虫错误处理示例
const { chromium } = require('playwright');

class RobustCrawler {
constructor() {
this.maxRetries = 3;
this.requestTimeout = 30000;
}

async crawl(url) {
const browser = await chromium.launch();
const results = [];

try {
  for (let retry = 1; retry <= this.maxRetries; retry++) {
    try {
      const page = await browser.newPage();
      
      // 设置超时
      page.setDefaultTimeout(this.requestTimeout);
      
      // 监听请求失败
      page.on('requestfailed', request => {
        console.warn(`请求失败: ${request.url()} - ${request.failure().errorText}`);
      });
      
      // 访问页面
      console.log(`尝试 ${retry}/${this.maxRetries}: 访问 ${url}`);
      const response = await page.goto(url, {
        waitUntil: 'domcontentloaded',
        timeout: this.requestTimeout
      });
      
      if (!response.ok()) {
        thrownewError(`HTTP ${response.status()}: ${response.statusText()}`);
      }
      
      // 提取数据
      const data = awaitthis.extractData(page);
      results.push(data);
      
      await page.close();
      break; // 成功则退出重试循环
      
    } catch (error) {
      console.log(`尝试 ${retry} 失败:`, error.message);
      
      if (retry === this.maxRetries) {
        thrownewError(`爬取 ${url} 失败: ${error.message}`);
      }
      
      // 指数退避
      awaitnewPromise(resolve =>
        setTimeout(resolve, 1000 * Math.pow(2, retry - 1))
      );
    }
  }
} finally {
  await browser.close();
}

return results;

}

async extractData(page) {
// 使用选择器重试提取数据
const extractWithRetry = async (selector, extractor) => {
for (let i = 0; i < 3; i++) {
try {
await page.waitForSelector(selector, { timeout: 5000 });
returnawait extractor();
} catch (error) {
if (i === 2) throw error;
await page.waitForTimeout(1000);
}
}
};

const title = await extractWithRetry('h1', async () => {
  returnawait page.textContent('h1');
});

return { title };

}
}
监控与日志记录
完善的错误处理还需要良好的监控:

class MonitoringErrorHandler {
constructor() {
this.errors = [];
this.stats = {
totalOperations: 0,
failedOperations: 0,
retriedOperations: 0,
recoveredOperations: 0
};
}

async trackOperation(operationName, operation) {
this.stats.totalOperations++;
const startTime = Date.now();

try {
  const result = await operation();
  return result;
} catch (error) {
  this.stats.failedOperations++;
  this.errors.push({
    operation: operationName,
    error: error.message,
    timestamp: newDate().toISOString(),
    duration: Date.now() - startTime
  });
  
  // 可以发送到监控系统
  this.reportToMonitoringSystem(error, operationName);
  
  throw error;
}

}

reportToMonitoringSystem(error, operationName) {
// 发送到Sentry, Datadog等
console.error([MONITORING] ${operationName} failed:, error.message);
}

getStats() {
return {
...this.stats,
successRate: ((this.stats.totalOperations - this.stats.failedOperations) /
this.stats.totalOperations * 100).toFixed(2) + '%'
};
}
}
最佳实践总结
分级处理策略:根据错误类型采取不同的重试策略,网络错误可以立即重试,业务错误可能需要延迟重试。

避免无限重试:始终设置最大重试次数,避免陷入死循环。

指数退避算法:重试间隔应逐渐增加,避免对目标服务器造成压力。

上下文隔离:每次重试前清理状态,确保测试的独立性。

详细日志记录:记录每次重试的上下文信息,便于问题排查。

监控集成:将错误信息集成到现有监控系统,实现主动告警。

用户可配置:将重试参数(次数、延迟等)设计为可配置项,适应不同场景需求。

结语
错误处理不是Playwright脚本的事后考虑,而是应该在设计初期就纳入架构的重要部分。一个健壮的脚本不仅要能完成任务,更要能优雅地处理失败。通过实现智能的重试机制,你的自动化脚本将能够在生产环境中稳定运行,显著减少人工干预的需要。

记住,好的错误处理机制是透明的一一当它正常工作时,用户几乎感觉不到它的存在;当问题出现时,它又能提供足够的信息帮助快速定位问题。这才是真正有价值的自动化解决方案。

推荐学习
Ai自动化智能体与工作流平台课程,限时免费,机会难得。扫码报名,参与直播,希望您在这场课程中收获满满,开启智能自动化测试的新篇章!

image

posted @ 2026-01-20 14:38  霍格沃兹测试开发学社  阅读(0)  评论(0)    收藏  举报