Что такое 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, пароль, возраст, страна.
- Создайте JSON-файл с 15+ тест-кейсами: валидные данные, граничные значения, невалидные данные
- Напишите параметризованный тест, читающий из JSON
- Включите минимум 3 теста граничных значений для возраста
- Включите минимум 3 негативных теста для валидации email
- Добавьте кросс-viewport тестирование для 3 размеров экрана
Ключевые выводы
- Data-driven тестирование устраняет дублирование кода через параметризацию входных данных
- Внешние источники (JSON, CSV) позволяют поддерживать данные независимо
- Используйте анализ граничных значений для создания осмысленных наборов данных
- Остерегайтесь комбинаторного взрыва — применяйте pairwise-тестирование
- Функции-фабрики создают гибкие и читаемые тестовые данные
- Переменные окружения переключают данные между dev, staging и production