Playwright测试环境配置:多环境切换与管理
【摘要】 1. 从一次凌晨三点的事故说起上个月,团队发生了一次令人头疼的线上问题——预生产环境的测试脚本竟然在生产环境上执行了,差点删除了真实用户数据。事后复盘发现,根本原因是环境配置混乱:有人把环境变量写死在代码里,有人用不同的命名方式,还有人直接在本地改了配置却没提交。这让我意识到,一个清晰、可靠的环境管理策略,不是“锦上添花”,而是自动化测试的“生命线”。今天,我就把我们团队趟过的坑、总结出的最...
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. 我们收获了什么
自从实施了这套环境管理方案,我们团队发生了这些变化:
-
新人上手时间从2天减少到2小时——"npm run test:dev"就能开始 -
**环境相关bug减少了80%**——再也没有"在我机器上是好的"这种问题 -
CI/CD流水线更加可靠——每个环境都有明确的配置 -
安全审计变得简单——所有凭证集中管理,不散落在代码中
8. 最后的建议
-
从简单开始:不必一开始就实现所有功能,先从分离dev和prod开始 -
团队共识很重要:确保团队成员都理解并遵守环境配置规范 -
定期清理:每季度回顾一次环境配置,删除不再需要的变量 -
文档!文档!文档!:维护一个CONFIGURATION.md文件
记住,好的环境配置不是一次性工作,而是一个持续改进的过程。先从解决你最痛的那个点开始,然后逐步完善。
希望这套方案能帮你避免我们曾经踩过的那些坑。如果有问题或者更好的建议,欢迎在评论区交流——测试工具的发展,离不开社区的分享与共创。
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱:
cloudbbs@huaweicloud.com
- 点赞
- 收藏
- 关注作者
评论(0)