霍格沃兹测试开发学社

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

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

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

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

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

  1. 环境配置的常见误区
    先看看这些似曾相识的场景:

反例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 # 到底该用哪个?
如果你正在用类似的方式,别担心——我们当初也是这样开始的。接下来,我会带你一步步建立一套优雅的解决方案。

  1. 搭建三层配置体系
    我们的目标是建立这样一个结构:

根目录/
├── .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);

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

image

  1. 运行脚本与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/
  1. 进阶技巧与最佳实践
    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文件
记住,好的环境配置不是一次性工作,而是一个持续改进的过程。先从解决你最痛的那个点开始,然后逐步完善。

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

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

image

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