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

举报
霍格沃兹测试开发学社 发表于 2026/01/19 17:22:02 2026/01/19
【摘要】 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: { width1280height720 },
      screenshot'only-on-failure',
      retries1,
      workers3,
      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, envthis.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({
    timeoutget('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文件

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

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

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。