Параметризация тестов

Урок: Параметризация тестов Playwright

Введение

Представь, что ты тестируешь форму логина на сайте. У тебя есть несколько сценариев: правильный логин и пароль, неправильный пароль, пустые поля, слишком короткий пароль. Самый простой путь — написать отдельный тест под каждый случай.

Но довольно быстро ты замечаешь проблему: тесты почти одинаковые. Отличаются только входные данные и ожидаемый результат. Код начинает дублироваться, становится длинным и сложным в поддержке.

Теперь представь, что ты можешь описать сам сценарий один раз, а набор входных данных просто «подставлять» в него. Это и есть параметризация.

В Playwright параметризация позволяет запускать один и тот же тест с разными данными. Это делает тесты короче, понятнее и гораздо удобнее в поддержке.

Давай разберёмся, как это работает.

Официальное руководство: Parameterize tests | Playwright. Там же — как не сломать хуки при forEach и как параметризовать проекты в конфиге.


Что такое параметризация и зачем она нужна

Параметризация — это способ запускать один тест с разными входными данными.

Вместо того чтобы писать так:

test('логин с валидными данными', async ({ page }) => {
  // ...
});

test('логин с неверным паролем', async ({ page }) => {
  // ...
});

test('логин с пустыми полями', async ({ page }) => {
  // ...
});

мы можем описать один тест и передать в него разные наборы данных.

В документации выделяют два уровня:

  1. Параметризация тестов — несколько кейсов в одном файле (часто массив + forEach / цикл при регистрации тестов).
  2. Параметризация проектов — одни и те же тесты прогоняются в разных проектах playwright.config с разными use / опциями (как отдельные «конфигурации»).

Главная идея: мы отделяем логику теста от данных для теста.

Это даёт сразу несколько преимуществ:

  • меньше дублирования кода;
  • легче добавлять новые кейсы;
  • тесты читаются как таблица сценариев;
  • проще поддерживать при изменениях.

Простейшая параметризация через массив данных

Самый базовый способ — использовать массив и forEach.

Пример

import { test, expect } from '@playwright/test';

const testData = [
  { login: 'user1', password: 'correct', success: true },
  { login: 'user1', password: 'wrong', success: false },
  { login: '', password: '', success: false },
];

testData.forEach(({ login, password, success }) => {
  test(`логин: ${login}, пароль: ${password}`, async ({ page }) => {
    await page.goto('https://example.com/login');

    await page.getByLabel('Логин').fill(login);
    await page.getByLabel('Пароль').fill(password);
    await page.getByRole('button', { name: 'Войти' }).click();

    if (success) {
      await expect(page).toHaveURL(/dashboard/);
    } else {
      await expect(page.getByRole('alert')).toBeVisible();
    }
  });
});

Что здесь происходит

  • Мы создаём массив testData с разными сценариями.
  • Каждый объект — это отдельный тест-кейс.
  • forEach проходит по массиву и создаёт тест для каждого набора данных.
  • Внутри теста используются значения из объекта.

Важный момент

test(`логин: ${login}, пароль: ${password}`, ...)

Название теста формируется динамически. Это помогает в отчётах — ты сразу видишь, какой кейс упал.


Как это работает внутри

Когда Playwright видит такой код:

testData.forEach(...)

он не выполняет тесты сразу. Он регистрирует тесты во время загрузки файла.

То есть:

  1. Node.js выполняет forEach;
  2. на каждой итерации создаётся новый test(...);
  3. Playwright сохраняет их как отдельные тесты.

В итоге ты получаешь несколько независимых тестов, а не один тест с циклом.

Это важно: каждый тест из параметризации — полноценный тест со своей изоляцией.


Хуки и forEach: где объявлять

Из документации:

  • beforeEach / afterEach / beforeAll / afterAll, которые должны выполниться один раз на файл для всех параметров, объявляют снаружи forEach. Иначе при регистрации хука внутри цикла он продублируется на каждую итерацию.
  • Если нужен свой beforeEach на каждый набор данных (например, разный goto с name в URL), оборачивают итерацию в test.describe + хуки внутри — см. второй пример в доке.

Улучшение читаемости: описание тест-кейсов

Когда данных становится больше, полезно делать их более выразительными.

Пример

const testData = [
  {
    name: 'валидный логин',
    login: 'user1',
    password: 'correct',
    success: true,
  },
  {
    name: 'неверный пароль',
    login: 'user1',
    password: 'wrong',
    success: false,
  },
];

Используем это в тесте:

testData.forEach(({ name, login, password, success }) => {
  test(name, async ({ page }) => {
    await page.goto('https://example.com/login');

    await page.getByLabel('Логин').fill(login);
    await page.getByLabel('Пароль').fill(password);
    await page.getByRole('button', { name: 'Войти' }).click();

    if (success) {
      await expect(page).toHaveURL(/dashboard/);
    } else {
      await expect(page.getByRole('alert')).toBeVisible();
    }
  });
});

Почему это лучше

  • Названия тестов становятся понятными;
  • не нужно читать данные внутри теста, чтобы понять сценарий;
  • отчёты становятся удобнее.

Параметризация с test.describe

Иногда нужно сгруппировать параметризованные тесты.

Пример

test.describe('Тесты логина', () => {
  const testData = [
    { login: 'user1', password: 'correct', success: true },
    { login: 'user1', password: 'wrong', success: false },
  ];

  testData.forEach(({ login, password, success }) => {
    test(`логин: ${login}, пароль: ${password}`, async ({ page }) => {
      await page.goto('https://example.com/login');

      await page.getByLabel('Логин').fill(login);
      await page.getByLabel('Пароль').fill(password);
      await page.getByRole('button', { name: 'Войти' }).click();

      if (success) {
        await expect(page).toHaveURL(/dashboard/);
      } else {
        await expect(page.getByRole('alert')).toBeVisible();
      }
    });
  });
});

Что даёт test.describe

  • объединяет тесты в логическую группу;
  • улучшает структуру отчётов;
  • позволяет применять общие настройки (например, beforeEach).

Параметризация на уровне проектов (playwright.config)

Один сценарий можно прогнать несколько раз с разными опциями, не дублируя файлы: вынеси параметр в кастомную опцию фикстур (test.extend с { option: true }) и задай разные значения в projects[].use в конфиге. В тесте параметр приходит как обычная фикстура рядом с page.

Идея на пальцах (полный код — в Parameterized Projects):

// my-test.js — один раз расширяешь test
import { test as base } from '@playwright/test';
export const test = base.extend({
  person: ['Гость', { option: true }],
});

// example.spec.js
import { test } from './my-test';
test('приветствие', async ({ page, person }) => {
  await page.goto(`/greet?name=${person}`);
  await expect(page.getByRole('heading')).toContainText(person);
});
// playwright.config.js — два проекта = два значения person
import { defineConfig } from '@playwright/test';

export default defineConfig({
  projects: [
    { name: 'alice', use: { person: 'Alice' } },
    { name: 'bob', use: { person: 'Bob' } },
  ],
});

Поведение кастомных опций в проектах менялось в 1.18 — см. release notes, если поддерживаешь старые конфиги.


Переменные окружения и .env

Данные и секреты удобно подставлять из окружения (process.env.USER_NAME в тесте или в defineConfig), а в CI — через USER_NAME=… npx playwright test. Для локальной разработки часто подключают dotenv в playwright.config и файл .env — см. Passing Environment Variables.


Генерация тестов из CSV

Раннер выполняется в Node.js, поэтому можно прочитать CSV с диска, распарсить (csv-parse и т.п.) и в цикле вызвать test('…', …) — пример в Create tests via a CSV file. Имена тестов должны оставаться уникальными.


Когда параметризация особенно полезна

Параметризация особенно хорошо работает в ситуациях:

  • формы (логин, регистрация, фильтры);
  • API-тесты с разными входными данными;
  • проверка валидаций;
  • тестирование ролей пользователей;
  • тесты на разные конфигурации (браузеры, языки, регионы).

Например, если нужно проверить 10 вариантов неправильного ввода — параметризация превращает это из 10 тестов в один аккуратный блок.


Ограничения и важные нюансы

Параметризация — мощный инструмент, но важно понимать границы:

  1. Не стоит превращать один тест в «комбайн» на 50 кейсов — отчёты станут неудобными.
  2. Если логика тестов сильно различается — лучше разделить их.
  3. Данные должны быть простыми и понятными — иначе параметризация теряет смысл.

Главное правило: параметризуем только то, что действительно одинаково по логике.


Практический смысл

В реальной работе параметризация:

  • ускоряет написание тестов;
  • снижает количество ошибок из-за копипаста;
  • упрощает поддержку при изменении UI или API;
  • делает тесты более декларативными — ты описываешь сценарии, а не переписываешь код.

В командах это особенно важно: другие разработчики и тестировщики быстрее понимают тесты.


Итоговое понимание

Параметризация — это способ отделить данные от одинаковой логики и не плодить копипаст.

По документации Playwright:

  • на уровне файла — массив + forEach при регистрации тестов, уникальные имена, продуманные хуки;
  • на уровне конфига — проекты и кастомные опции фикстур;
  • снаружи — переменные окружения / CSV для больших таблиц кейсов.

Если сценарии начинают расходиться по веткам — не насилуй одну мега-функцию: разнеси по тестам или файлам. Тогда параметризация остаётся читаемой и устойчивой в отчётах.