Что такое Data-Driven тестирование?

Data-driven тестирование отделяет логику теста от тестовых данных. Вместо отдельного теста для каждой комбинации вы пишете один тест, запускаемый с разными наборами данных.

Без data-driven подхода (5 отдельных тестов):

test('логин с данными админа', async ({ page }) => {
  await loginPage.login('admin@test.com', 'AdminPass1');
  await expect(page).toHaveURL('/dashboard');
});

test('логин с данными редактора', async ({ page }) => {
  await loginPage.login('editor@test.com', 'EditorPass1');
  await expect(page).toHaveURL('/dashboard');
});
// ... ещё 3 почти идентичных теста

С data-driven подходом (1 тест, 5 наборов данных):

const validUsers = [
  { email: 'admin@test.com', password: 'AdminPass1', role: 'admin' },
  { email: 'editor@test.com', password: 'EditorPass1', role: 'editor' },
  { email: 'viewer@test.com', password: 'ViewerPass1', role: 'viewer' },
  { email: 'manager@test.com', password: 'MgrPass1', role: 'manager' },
  { email: 'support@test.com', password: 'SupportPass1', role: 'support' },
];

for (const user of validUsers) {
  test(`логин с данными ${user.role}`, async ({ page }) => {
    await loginPage.login(user.email, user.password);
    await expect(page).toHaveURL('/dashboard');
  });
}

Data-Driven тестирование в Playwright

Через test.describe и циклы

const loginScenarios = [
  { email: 'admin@test.com', password: 'valid', expectSuccess: true },
  { email: 'admin@test.com', password: 'wrong', expectSuccess: false },
  { email: '', password: 'valid', expectSuccess: false },
  { email: 'invalid-format', password: 'valid', expectSuccess: false },
  { email: 'locked@test.com', password: 'valid', expectSuccess: false },
];

test.describe('Сценарии логина', () => {
  for (const scenario of loginScenarios) {
    test(`логин с email="${scenario.email}" должен ${scenario.expectSuccess ? 'успешно пройти' : 'провалиться'}`, async ({ page }) => {
      const loginPage = new LoginPage(page);
      await loginPage.goto();
      await loginPage.login(scenario.email, scenario.password);

      if (scenario.expectSuccess) {
        await expect(page).toHaveURL('/dashboard');
      } else {
        await expect(loginPage.errorMessage).toBeVisible();
      }
    });
  }
});

Внешние JSON-файлы

{
  "validUsers": [
    { "email": "admin@test.com", "password": "AdminPass1", "role": "admin" },
    { "email": "editor@test.com", "password": "EditorPass1", "role": "editor" }
  ],
  "invalidCredentials": [
    { "email": "wrong@test.com", "password": "wrong", "error": "Неверные учётные данные" },
    { "email": "", "password": "", "error": "Email обязателен" }
  ]
}
import testData from './test-data/users.json';

test.describe('Валидный логин', () => {
  for (const user of testData.validUsers) {
    test(`${user.role} может войти`, async ({ page }) => {
      const loginPage = new LoginPage(page);
      await loginPage.goto();
      await loginPage.login(user.email, user.password);
      await expect(page).toHaveURL('/dashboard');
    });
  }
});

CSV-файлы

import fs from 'fs';

function loadCSV(filePath) {
  const content = fs.readFileSync(filePath, 'utf-8');
  const lines = content.trim().split('\n');
  const headers = lines[0].split(',');
  return lines.slice(1).map(line => {
    const values = line.split(',');
    return headers.reduce((obj, header, i) => {
      obj[header.trim()] = values[i].trim();
      return obj;
    }, {});
  });
}

const priceData = loadCSV('test-data/prices.csv');

for (const row of priceData) {
  test(`товар ${row.name} имеет цену ${row.expectedPrice}`, async ({ page }) => {
    await page.goto(`/products/${row.slug}`);
    const price = await page.textContent('.price');
    expect(price).toBe(row.expectedPrice);
  });
}

Распространённые Data-Driven паттерны

Тестирование граничных значений

const ageValidation = [
  { age: -1, valid: false, desc: 'отрицательное' },
  { age: 0, valid: false, desc: 'ноль' },
  { age: 1, valid: true, desc: 'минимально допустимое' },
  { age: 17, valid: false, desc: 'несовершеннолетний' },
  { age: 18, valid: true, desc: 'точно минимальный возраст' },
  { age: 120, valid: true, desc: 'максимально допустимое' },
  { age: 121, valid: false, desc: 'выше максимума' },
];

for (const { age, valid, desc } of ageValidation) {
  test(`возраст ${age} (${desc}) должен быть ${valid ? 'принят' : 'отклонён'}`, async ({ page }) => {
    await registrationPage.enterAge(age);
    await registrationPage.submit();
    if (valid) {
      await expect(registrationPage.successMessage).toBeVisible();
    } else {
      await expect(registrationPage.errorMessage).toBeVisible();
    }
  });
}

Кросс-viewport данные

const viewports = [
  { width: 375, height: 667, name: 'iPhone SE' },
  { width: 768, height: 1024, name: 'iPad' },
  { width: 1920, height: 1080, name: 'Desktop' },
];

for (const viewport of viewports) {
  test(`главная корректно отображается на ${viewport.name}`, async ({ page }) => {
    await page.setViewportSize({ width: viewport.width, height: viewport.height });
    await page.goto('/');
    await expect(page.locator('.hero')).toBeVisible();
  });
}

Данные на основе окружения

Переменные окружения для переключения наборов данных:

const environments = {
  dev: {
    baseUrl: 'https://dev.example.com',
    adminEmail: 'admin@dev.test.com',
    adminPassword: 'DevPass123',
  },
  staging: {
    baseUrl: 'https://staging.example.com',
    adminEmail: 'admin@staging.test.com',
    adminPassword: 'StagingPass123',
  },
};

const env = environments[process.env.TEST_ENV || 'dev'];

test('админ может открыть дашборд', async ({ page }) => {
  await page.goto(env.baseUrl);
  await loginPage.login(env.adminEmail, env.adminPassword);
  await expect(page).toHaveURL(`${env.baseUrl}/dashboard`);
});

Борьба с комбинаторным взрывом

При множестве параметров число комбинаций растёт экспоненциально. Pairwise-тестирование сокращает комбинации, сохраняя покрытие.

Проблема

5 полей × 10 значений = 100 000 комбинаций. Тестировать все непрактично.

Решение: Pairwise-тестирование

Вместо всех комбинаций — тестирование каждой пары значений хотя бы раз. Обычно достаточно 50-100 тест-кейсов.

const checkoutData = [
  { payment: 'visa', shipping: 'standard', currency: 'USD' },
  { payment: 'visa', shipping: 'express', currency: 'EUR' },
  { payment: 'mastercard', shipping: 'standard', currency: 'EUR' },
  { payment: 'mastercard', shipping: 'express', currency: 'USD' },
  { payment: 'paypal', shipping: 'standard', currency: 'USD' },
  { payment: 'paypal', shipping: 'express', currency: 'EUR' },
];

Фабрики тестовых данных

Для сложных данных используйте функции-фабрики:

function createOrder(overrides = {}) {
  return {
    product: 'Widget',
    quantity: 1,
    price: 29.99,
    currency: 'USD',
    shipping: 'standard',
    ...overrides,
  };
}

const orders = [
  createOrder({ quantity: 1, shipping: 'standard' }),
  createOrder({ quantity: 100, shipping: 'express' }),
  createOrder({ quantity: 0, price: 0 }),
  createOrder({ currency: 'EUR', price: 24.99 }),
];

Упражнение: Постройте Data-Driven тесты

Создайте data-driven suite для формы регистрации с полями: имя, email, пароль, возраст, страна.

  1. Создайте JSON-файл с 15+ тест-кейсами: валидные данные, граничные значения, невалидные данные
  2. Напишите параметризованный тест, читающий из JSON
  3. Включите минимум 3 теста граничных значений для возраста
  4. Включите минимум 3 негативных теста для валидации email
  5. Добавьте кросс-viewport тестирование для 3 размеров экрана

Ключевые выводы

  • Data-driven тестирование устраняет дублирование кода через параметризацию входных данных
  • Внешние источники (JSON, CSV) позволяют поддерживать данные независимо
  • Используйте анализ граничных значений для создания осмысленных наборов данных
  • Остерегайтесь комбинаторного взрыва — применяйте pairwise-тестирование
  • Функции-фабрики создают гибкие и читаемые тестовые данные
  • Переменные окружения переключают данные между dev, staging и production