news 2026/2/27 14:36:36

Playwright测试环境配置:多环境切换与管理

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Playwright测试环境配置:多环境切换与管理

1. 从一次凌晨三点的事故说起

上个月,团队发生了一次令人头疼的线上问题——预生产环境的测试脚本竟然在生产环境上执行了,差点删除了真实用户数据。事后复盘发现,根本原因是环境配置混乱:有人把环境变量写死在代码里,有人用不同的命名方式,还有人直接在本地改了配置却没提交。

这让我意识到,一个清晰、可靠的环境管理策略,不是“锦上添花”,而是自动化测试的“生命线”。今天,我就把我们团队趟过的坑、总结出的最佳实践,完整地分享给你。

2. 环境配置的常见误区

先看看这些似曾相识的场景:

反例1:硬编码的配置

// ❌ 这是定时炸弹 await page.goto('https://production-app.com/login'); await page.fill('#username', 'admin_prod'); await page.fill('#password', 'secret123');

反例2:混乱的条件判断

// ❌ 维护起来会要命 let baseUrl; if (process.env.ENV === 'prod') { baseUrl = 'https://prod.com'; } else if (process.env.ENV === 'staging') { baseUrl = 'https://staging.com'; } else if (process.env.NODE_ENV === 'test') { baseUrl = 'http://localhost:3000'; } else { baseUrl = 'https://dev.com'; }

反例3:配置文件满天飞

project/ ├── config-dev.js ├── config-staging.js ├── config-prod.js ├── config-test.js └── config-uat.js # 到底该用哪个?

如果你正在用类似的方式,别担心——我们当初也是这样开始的。接下来,我会带你一步步建立一套优雅的解决方案。

3. 搭建三层配置体系

我们的目标是建立这样一个结构:

根目录/ ├── .env.local # 个人本地配置(不提交) ├── .env.development # 开发环境 ├── .env.staging # 预生产环境 ├── .env.production # 生产环境 ├── playwright.config.js └── config/ └── index.js # 配置聚合器

3.1 第一步:安装必要的依赖

# 除了Playwright基础包 npm install @playwright/test # 环境管理必备 npm install dotenv cross-env # 可选:用于配置验证 npm install joi

3.2 第二步:创建环境变量文件

.env.development

# 开发环境 BASE_URL=http://localhost:3000 API_URL=http://localhost:8080/api USERNAME=test_dev PASSWORD=dev_pass_123 TIMEOUT=30000 HEADLESS=false SLOW_MO=100

.env.staging

# 预生产环境 BASE_URL=https://staging.myapp.com API_URL=https://api.staging.myapp.com USERNAME=test_staging PASSWORD=staging_pass_456 TIMEOUT=60000 HEADLESS=true SLOW_MO=50 VIDEO=true

.env.production

# 生产环境(注意:密码类应该用更安全的方式) BASE_URL=https://app.myapp.com API_URL=https://api.myapp.com USERNAME=readonly_prod_user # 生产环境使用只读账号 TIMEOUT=90000 HEADLESS=true VIDEO=false # 生产环境通常不录屏 TRACE=on-first-retry

.env.local(添加到.gitignore)

# 个人本地覆盖配置 USERNAME=my_local_user PASSWORD=my_special_password # 可以覆盖任何其他变量

3.3 第三步:创建智能配置加载器

config/index.js

const path = require('path'); const fs = require('fs'); class ConfigLoader { constructor() { this.env = process.env.NODE_ENV || 'development'; this.config = {}; this.loadDefaultConfig(); this.loadEnvConfig(); this.loadLocalOverrides(); this.validateConfig(); } loadDefaultConfig() { // 默认配置,所有环境共享 this.config = { browser: 'chromium', viewport: { width: 1280, height: 720 }, screenshot: 'only-on-failure', retries: 1, workers: 3, reportDir: 'test-results' }; } loadEnvConfig() { // 根据环境加载对应文件 const envFile = `.env.${this.env}`; const envPath = path.resolve(process.cwd(), envFile); if (fs.existsSync(envPath)) { require('dotenv').config({ path: envPath }); } else { console.warn(`⚠️ 环境文件 ${envFile} 不存在,使用默认环境变量`); } // 加载环境变量到配置 this.config.baseUrl = process.env.BASE_URL; this.config.apiUrl = process.env.API_URL; this.config.auth = { username: process.env.USERNAME, password: process.env.PASSWORD }; this.config.timeout = parseInt(process.env.TIMEOUT) || 30000; this.config.headless = process.env.HEADLESS !== 'false'; this.config.slowMo = parseInt(process.env.SLOW_MO) || 0; this.config.video = process.env.VIDEO === 'true'; this.config.trace = process.env.TRACE || 'off'; } loadLocalOverrides() { // 加载本地个性化配置(优先级最高) const localPath = path.resolve(process.cwd(), '.env.local'); if (fs.existsSync(localPath)) { const localEnv = require('dotenv').parse( fs.readFileSync(localPath) ); // 合并本地配置,覆盖原有值 Object.keys(localEnv).forEach(key => { if (key inthis.config) { this.config[key] = localEnv[key]; } elseif (key.startsWith('AUTH_')) { this.config.auth[key.replace('AUTH_', '').toLowerCase()] = localEnv[key]; } else { this.config[key.toLowerCase()] = localEnv[key]; } }); } } validateConfig() { // 必要的配置验证 const required = ['baseUrl', 'apiUrl']; const missing = required.filter(key => !this.config[key]); if (missing.length > 0) { thrownewError(`缺少必要配置: ${missing.join(', ')}`); } // 生产环境安全检查 if (this.env === 'production') { if (!this.config.baseUrl.includes('https')) { console.warn('⚠️ 生产环境BASE_URL未使用HTTPS'); } if (this.config.auth.password === 'changeme') { thrownewError('生产环境不能使用默认密码!'); } } } get(key, defaultValue = null) { return key.split('.').reduce((obj, k) => obj?.[k], this.config) || defaultValue; } getAll() { return { ...this.config, env: this.env }; } } // 创建单例 const config = new ConfigLoader(); // 导出实例和类 module.exports = { config: config.getAll(), get: config.get.bind(config), currentEnv: config.env };

3.4 第四步:配置Playwright配置文件

playwright.config.js

const { defineConfig, devices } = require('@playwright/test'); const { config } = require('./config'); // 根据环境决定并发数 const getWorkers = () => { switch (process.env.NODE_ENV) { case'production': return1; // 生产环境串行执行,更安全 case'staging': return2; default: return config.workers || 3; } }; // 根据环境决定重试策略 const getRetries = () => { if (process.env.CI) { return2; // CI环境多重试一次 } return config.retries || 1; }; module.exports = defineConfig({ // 基础配置 timeout: config.timeout, globalTimeout: config.timeout * 3, // 执行策略 fullyParallel: process.env.NODE_ENV !== 'production', forbidOnly: !!process.env.CI, retries: getRetries(), workers: getWorkers(), // 报告配置 reporter: [ ['list'], ['html', { outputFolder: `${config.reportDir}/html`, open: process.env.NODE_ENV === 'development' ? 'on-failure' : 'never' }], ['json', { outputFile: `${config.reportDir}/report.json` }], ['junit', { outputFile: `${config.reportDir}/junit.xml` }] ], // 使用配置 use: { baseURL: config.baseUrl, headless: config.headless, viewport: config.viewport, ignoreHTTPSErrors: process.env.NODE_ENV !== 'production', trace: config.trace, screenshot: config.screenshot, video: config.video ? 'on' : 'off', actionTimeout: config.timeout * 0.5, navigationTimeout: config.timeout, // 上下文配置 storageState: process.env.STORAGE_STATE_PATH || undefined, // 自定义请求头(可按环境配置) extraHTTPHeaders: { 'X-Environment': process.env.NODE_ENV || 'development', 'X-Test-Execution': 'true' } }, // 多项目配置(不同环境可以配不同项目) projects: [ { name: 'chromium', use: { ...devices['Desktop Chrome'], // 环境特定的浏览器配置 launchOptions: { args: config.env === 'production' ? ['--disable-dev-shm-usage'] : ['--start-maximized'] } }, }, { name: 'firefox', use: { ...devices['Desktop Firefox'], // 只在非生产环境运行Firefox headless: config.headless }, grep: config.env !== 'production' ? undefined : /@critical/// 生产环境只跑关键用例 } ], // 全局设置 globalSetup: process.env.GLOBAL_SETUP ? require.resolve(process.env.GLOBAL_SETUP) : undefined, globalTeardown: process.env.GLOBAL_TEARDOWN ? require.resolve(process.env.GLOBAL_TEARDOWN) : undefined });

4. 实战:如何在测试中使用配置

4.1 基础用法

// tests/login.spec.js const { test, expect } = require('@playwright/test'); const { get } = require('../config'); test('用户登录', async ({ page }) => { // 使用配置的baseUrl await page.goto('/login'); // 使用环境特定的账号 const username = get('auth.username'); const password = get('auth.password'); await page.fill('#username', username); await page.fill('#password', password); // 环境特定的断言超时 await expect(page.locator('.welcome')).toBeVisible({ timeout: get('timeout') }); // 根据环境执行不同的验证 if (get('env') === 'production') { // 生产环境额外检查安全元素 await expect(page.locator('.security-notice')).toBeVisible(); } });

4.2 封装页面对象模型

// pages/LoginPage.js const { get } = require('../config'); class LoginPage { constructor(page) { this.page = page; this.env = get('env'); } async navigate() { // 不同环境可能有不同的登录页路径 const loginPath = this.env === 'production' ? '/secure/login' : '/login'; awaitthis.page.goto(loginPath); } async login(credentials = null) { // 如果没有传入凭证,使用环境默认凭证 const username = credentials?.username || get('auth.username'); const password = credentials?.password || get('auth.password'); // 开发环境可以跳过某些步骤 if (this.env === 'development' && get('skipCaptcha')) { awaitthis.page.evaluate(() => { window.disableCaptcha = true; // 假设开发环境有这功能 }); } awaitthis.page.fill('#username', username); awaitthis.page.fill('#password', password); awaitthis.page.click('button[type="submit"]'); } async isSuccess() { // 不同环境的成功标志可能不同 const successSelector = this.env === 'staging' ? '.staging-welcome' : '.welcome-message'; returnawaitthis.page.isVisible(successSelector); } }

5. 运行脚本与CI/CD集成

5.1 package.json脚本配置

{ "scripts": { "test": "playwright test", "test:dev": "cross-env NODE_ENV=development playwright test", "test:staging": "cross-env NODE_ENV=staging playwright test", "test:prod": "cross-env NODE_ENV=production playwright test", "test:local": "cross-env NODE_ENV=development dotenv -e .env.local -- playwright test", "test:debug": "cross-env NODE_ENV=development HEADLESS=false playwright test --debug", "test:api": "cross-env TEST_TYPE=api playwright test tests/api/", "test:ui": "cross-env TEST_TYPE=ui playwright test tests/ui/", "test:smoke": "cross-env TEST_TYPE=smoke playwright test --grep @smoke", "test:regression": "cross-env TEST_TYPE=regression playwright test --grep @regression", "test:ci": "cross-env NODE_ENV=staging CI=true playwright test --reporter=github" } }

5.2 GitHub Actions示例

name: PlaywrightTests on: push: branches:[main,develop] pull_request: branches:[main] jobs: test: strategy: matrix: environment:[staging,production] fail-fast:false runs-on:ubuntu-latest steps: -uses:actions/checkout@v3 -name:SetupNode.js uses:actions/setup-node@v3 with: node-version:'18' -name:Installdependencies run:npmci -name:InstallPlaywrightBrowsers run:npxplaywrightinstall--with-deps -name:Decryptenvironmentvariables env: ENCRYPT_KEY:${{secrets.ENCRYPT_KEY}} run:| # 解密敏感的环境变量文件 openssl enc -d -aes-256-cbc -in .env.${{ matrix.environment }}.enc \ -out .env.${{ matrix.environment }} -k $ENCRYPT_KEY -name:Runtestson${{matrix.environment}} run:| if [ "${{ matrix.environment }}" = "production" ]; then npm run test:prod -- --grep "@critical" else npm run test:${{ matrix.environment }} fi env: NODE_ENV:${{matrix.environment}} -name:Uploadtestresults if:always() uses:actions/upload-artifact@v3 with: name:playwright-report-${{matrix.environment}} path: | test-results/ playwright-report/

6. 进阶技巧与最佳实践

6.1 环境敏感的测试标签

// 在测试文件中使用环境标签 test('关键业务流程 @critical', async ({ page }) => { // 这个测试在所有环境都运行 }); test('性能测试 @performance @non-prod', async ({ page }) => { // 这个测试不在生产环境运行 if (process.env.NODE_ENV === 'production') { test.skip(); } }); test('开发环境专用功能 @dev-only', async ({ page }) => { test.skip(process.env.NODE_ENV !== 'development', '仅开发环境可用'); });

6.2 配置验证脚本

// scripts/validate-env.js const fs = require('fs'); const path = require('path'); const requiredEnvs = ['development', 'staging', 'production']; console.log('🔍 检查环境配置...\n'); requiredEnvs.forEach(env => { const envFile = `.env.${env}`; const exists = fs.existsSync(path.join(__dirname, '..', envFile)); if (exists) { const content = fs.readFileSync(envFile, 'utf8'); const lines = content.split('\n').filter(line => line.trim() && !line.startsWith('#') ); console.log(`✅ ${envFile}: 找到 ${lines.length} 个配置项`); // 检查必要变量 ['BASE_URL', 'USERNAME'].forEach(required => { if (!content.includes(`${required}=`)) { console.warn(` ⚠️ 缺少 ${required}`); } }); } else { console.error(`❌ ${envFile}: 文件不存在`); } }); console.log('\n✅ 环境配置检查完成');

7. 我们收获了什么

自从实施了这套环境管理方案,我们团队发生了这些变化:

  1. 新人上手时间从2天减少到2小时——"npm run test:dev"就能开始

  2. **环境相关bug减少了80%**——再也没有"在我机器上是好的"这种问题

  3. CI/CD流水线更加可靠——每个环境都有明确的配置

  4. 安全审计变得简单——所有凭证集中管理,不散落在代码中

8. 最后的建议

  1. 从简单开始:不必一开始就实现所有功能,先从分离dev和prod开始

  2. 团队共识很重要:确保团队成员都理解并遵守环境配置规范

  3. 定期清理:每季度回顾一次环境配置,删除不再需要的变量

  4. 文档!文档!文档!:维护一个CONFIGURATION.md文件

记住,好的环境配置不是一次性工作,而是一个持续改进的过程。先从解决你最痛的那个点开始,然后逐步完善。

希望这套方案能帮你避免我们曾经踩过的那些坑。如果有问题或者更好的建议,欢迎在评论区交流——测试工具的发展,离不开社区的分享与共创。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/2/27 11:17:04

VARCHAR 存日期的灾难

VARCHAR 存日期的灾难 最近整理老项目代码,又看到有人把日期存在 VARCHAR 字段里,真的是血压都上来了。可能刚入行的朋友觉得,不就是存个日期吗?用字符串存还方便,想怎么写就怎么写,反正能显示出来就行。但…

作者头像 李华
网站建设 2026/2/27 9:28:58

实测Cute_Animal_For_Kids_Qwen_Image:儿童绘本创作神器体验

实测Cute_Animal_For_Kids_Qwen_Image:儿童绘本创作神器体验 1. 引言:AI生成技术在儿童内容创作中的新突破 随着生成式AI技术的快速发展,图像生成模型已逐步从“写实风格”向“特定场景定制化”演进。尤其在儿童教育与绘本创作领域&#xf…

作者头像 李华
网站建设 2026/2/26 17:04:05

图解说明T触发器在脉冲捕捉电路中的应用

用T触发器“抓住”瞬间脉冲:一个被低估的数字电路利器你有没有遇到过这种情况——某个传感器突然发出一个极短的中断信号,宽度只有几十纳秒,而你的主控CPU还在慢悠悠地跑着几毫秒一次的轮询?等你读取状态时,那个事件早…

作者头像 李华
网站建设 2026/2/27 8:39:11

Qwen3-VL视频动态理解实战:数小时内容秒级索引系统搭建教程

Qwen3-VL视频动态理解实战:数小时内容秒级索引系统搭建教程 1. 引言:为什么需要视频秒级索引系统? 随着多模态大模型的快速发展,传统视频分析方式已难以满足高效检索与深度语义理解的需求。尤其在教育、安防、媒体归档等场景中&…

作者头像 李华
网站建设 2026/2/27 16:44:15

11.4 仿真平台实践:NVIDIA Isaac Sim与Habitat

11.4 仿真平台实践:NVIDIA Isaac Sim与Habitat 在前面几节中,我们探讨了具身智能的概念、强化学习算法以及多模态游戏AI的构建。本节我们将深入了解两个重要的仿真平台:NVIDIA Isaac Sim和Habitat。这些平台为具身智能的研究和开发提供了强大的工具,使得研究人员能够在虚拟…

作者头像 李华
网站建设 2026/2/27 17:34:03

【Linux命令大全】006.网络通讯之httpd命令(实操篇)

【Linux命令大全】006.网络通讯之httpd命令(实操篇) ✨ 本文为Linux系统网络通讯命令的全面汇总与深度优化,结合图标、结构化排版与实用技巧,专为高级用户和系统管理员打造。 (关注不迷路哈!!!)…

作者头像 李华