process.env.VARIABLE возвращает string | undefined в TypeScript, поэтому тест с незаданной переменной окружения не падает с понятной ошибкой при старте: он падает в середине с криптовым сообщением от локатора или аутентификации. Решение: обёртка requireEnv() которая бросает исключение на старте с именем переменной. Отсутствующий TEST_USER_EMAIL останавливает сьют до первого теста и говорит что именно нужно задать. Эта статья разбирает настройку dotenv с .env и .env.local, паттерн типобезопасного объекта env, валидацию в глобальном setup и разницу между vars. и secrets. в GitHub Actions для несекретных и секретных значений.

Что куда

В playwright.config.ts (коммитится в репозиторий):

export default defineConfig({
  timeout: 30000,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 4 : undefined,
  reporter: [['html'], ['list']],
  use: {
    baseURL: process.env.BASE_URL || 'http://localhost:3000',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
  },
});

В .env-файлах или CI-секретах (секреты никогда не коммитятся):

BASE_URL=https://staging.myapp.com
API_KEY=sk-test-abc123
TEST_USER_EMAIL=testuser@example.com
TEST_USER_PASSWORD=SecurePass123!
DATABASE_URL=postgresql://localhost:5432/testdb

Настройка dotenv

npm install -D dotenv

Создай .env с безопасными дефолтами (без реальных учётных данных):

# .env
BASE_URL=http://localhost:3000
TEST_USER_EMAIL=test@example.com
TEST_USER_PASSWORD=
DATABASE_URL=

Создай .env.local с реальными локальными значениями (в .gitignore):

# .env.local — никогда не коммитится
TEST_USER_EMAIL=realtest@example.com
TEST_USER_PASSWORD=RealPassword123
DATABASE_URL=postgresql://localhost:5432/myapp_test

Загрузка в конфиге:

// playwright.config.ts
import dotenv from 'dotenv';
import path from 'path';

// сначала .env.local (переопределяет .env)
dotenv.config({ path: path.resolve(__dirname, '.env.local') });
dotenv.config({ path: path.resolve(__dirname, '.env') });

export default defineConfig({
  use: {
    baseURL: process.env.BASE_URL || 'http://localhost:3000',
  },
});

Добавь в .gitignore:

.env.local
playwright/.auth/
test-results/
playwright-report/

Типобезопасные переменные окружения

process.env.VARIABLE возвращает string | undefined. Ловить отсутствующие переменные во время выполнения теста плохо. Лови при старте:

// utils/env.ts
function requireEnv(name: string): string {
  const value = process.env[name];
  if (!value) {
    throw new Error(`Environment variable ${name} is required but not set`);
  }
  return value;
}

export const env = {
  baseURL: process.env.BASE_URL || 'http://localhost:3000',
  apiKey: requireEnv('API_KEY'),
  testUser: {
    email: requireEnv('TEST_USER_EMAIL'),
    password: requireEnv('TEST_USER_PASSWORD'),
  },
};

Используй в тестах:

import { env } from '../utils/env';

test('login with test credentials', async ({ page }) => {
  await page.goto('/login');
  await page.getByLabel('Email').fill(env.testUser.email);
  await page.getByLabel('Password').fill(env.testUser.password);
});

Если TEST_USER_EMAIL не задан, получаешь понятную ошибку при старте: Environment variable TEST_USER_EMAIL is required but not set, а не загадочное падение в середине теста.

Конфигурация для нескольких окружений

Для нескольких тестовых окружений используй Playwright projects с разными настройками:

// playwright.config.ts
const environments = {
  local: {
    baseURL: 'http://localhost:3000',
    apiURL: 'http://localhost:8000',
  },
  staging: {
    baseURL: 'https://staging.myapp.com',
    apiURL: 'https://api-staging.myapp.com',
  },
  production: {
    baseURL: 'https://myapp.com',
    apiURL: 'https://api.myapp.com',
  },
};

const env = (process.env.TEST_ENV as keyof typeof environments) || 'local';
const config = environments[env];

export default defineConfig({
  use: {
    baseURL: config.baseURL,
    extraHTTPHeaders: {
      'X-API-Base': config.apiURL,
    },
  },
});

Запуск:

TEST_ENV=staging npx playwright test
TEST_ENV=production npx playwright test --grep @smoke

Флаги фич в тестах

Когда приложение использует флаги фич, тесты могут вести себя по-разному в зависимости от того что включено:

// проверяем включена ли фича через переменную окружения
const NEW_CHECKOUT = process.env.FEATURE_NEW_CHECKOUT === 'true';

test('checkout flow', async ({ page }) => {
  await page.goto('/checkout');

  if (NEW_CHECKOUT) {
    // тестируем новый UI чекаута
    await expect(page.getByTestId('new-checkout-form')).toBeVisible();
  } else {
    // тестируем legacy UI чекаута
    await expect(page.getByTestId('legacy-checkout-form')).toBeVisible();
  }
});

Лучше: отдельные тест-файлы для каждого состояния флага, управляемые через Playwright projects:

projects: [
  {
    name: 'new-checkout',
    testMatch: '**/checkout-new/**',
    use: { extraHTTPHeaders: { 'X-Feature-New-Checkout': 'true' } },
  },
  {
    name: 'legacy-checkout',
    testMatch: '**/checkout-legacy/**',
  },
],

Переменные окружения в CI/CD

Пример GitHub Actions с правильной обработкой секретов:

# .github/workflows/tests.yml
env:
  # несекретное — используй vars, видно в логах
  BASE_URL: ${{ vars.STAGING_URL }}
  TEST_ENV: staging

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Run tests
        env:
          # секретное — используй secrets, маскируется в логах
          TEST_USER_EMAIL: ${{ secrets.TEST_USER_EMAIL }}
          TEST_USER_PASSWORD: ${{ secrets.TEST_USER_PASSWORD }}
          API_KEY: ${{ secrets.API_KEY }}
        run: npx playwright test

Ключевое различие: vars. это несекретная конфигурация, видна в логах воркфлоу. secrets. маскируется в логах, никогда не печатается, нужна для учётных данных и ключей.

Валидация конфигурации при старте сьюта

Используй глобальный setup для проверки окружения до запуска тестов:

// global-setup.ts
export default async function globalSetup() {
  const required = ['TEST_USER_EMAIL', 'TEST_USER_PASSWORD', 'BASE_URL'];
  const missing = required.filter(key => !process.env[key]);

  if (missing.length > 0) {
    throw new Error(
      `Missing required environment variables:\n${missing.map(k => `  - ${k}`).join('\n')}\n\n` +
      `Copy .env.example to .env.local and fill in the values.`
    );
  }

  console.log(`Running tests against: ${process.env.BASE_URL}`);
}

Падает быстро с понятным сообщением вместо того чтобы дать первому тесту упасть с загадочной ошибкой локатора.

→ See also: Файл конфигурации Playwright: все опции, которые нужно знать | Конфигурация окружений Playwright: локальная, стейджинг и продакшн | GitHub Actions для тестов Playwright: полная настройка (2026)