Тест-дизайн: эквивалентные классы
Equivalence Partitioning (разбиение на классы эквивалентности) — техника тест-дизайна, при которой входные данные делятся на группы, где все значения группы должны обрабатываться одинаково — достаточно протестировать одно значение из каждой группы.
Зачем нужно
Нельзя протестировать все возможные входные данные. Если функция одинаково обрабатывает любое значение из диапазона 1-100, нет смысла проверять 100 вариантов — достаточно одного. Это сокращает количество тестов без потери эффективности.
Где используется
- Валидация форм: email, возраст, сумма
- Категоризация: группы пользователей, уровни доступа, тарифы
- Обработка типов данных: числа, строки, объекты, null
Основной контент
Принцип
Весь диапазон входных данных делится на классы:
- Валидные классы — система обрабатывает успешно
- Невалидные классы — система должна вернуть ошибку
Из каждого класса выбирается одно представительное значение.
Пример: классификация BMI
function getBMICategory(bmi) {
if (bmi < 18.5) return 'underweight';
if (bmi < 25) return 'normal';
if (bmi < 30) return 'overweight';
return 'obese';
}
// Классы эквивалентности:
// 1. bmi < 18.5 → 'underweight' (представитель: 16)
// 2. 18.5 ≤ bmi < 25 → 'normal' (представитель: 22)
// 3. 25 ≤ bmi < 30 → 'overweight' (представитель: 27)
// 4. bmi ≥ 30 → 'obese' (представитель: 35)
test.each([
[16, 'underweight'],
[22, 'normal'],
[27, 'overweight'],
[35, 'obese'],
])('BMI %d → %s', (bmi, expected) => {
expect(getBMICategory(bmi)).toBe(expected);
});
Пример: валидация email
function isValidEmail(email: string): boolean {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
// Классы эквивалентности:
// Валидные:
// 1. Стандартный email: user@example.com
// 2. С субдоменом: user@mail.example.com
// 3. С точкой в имени: first.last@example.com
// Невалидные:
// 4. Нет @: userexample.com
// 5. Нет домена: user@
// 6. Нет расширения: user@example
// 7. Пустая строка: ''
test.each([
// Валидные классы
['user@example.com', true],
['user@mail.example.com', true],
['first.last@example.com', true],
// Невалидные классы
['userexample.com', false],
['user@', false],
['user@example', false],
['', false],
])('isValidEmail("%s") → %s', (email, expected) => {
expect(isValidEmail(email)).toBe(expected);
});
Пример: скидка по возрасту
function getDiscount(age: number): number {
if (age < 0 || !Number.isInteger(age)) throw new Error('Invalid age');
if (age < 12) return 100; // дети бесплатно
if (age < 18) return 50; // скидка детям
if (age < 65) return 0; // без скидки
return 30; // пенсионерам
}
// 5 валидных классов + 1 невалидный (отрицательный возраст)
test.each([
[5, 100], // ребёнок
[15, 50], // подросток
[30, 0], // взрослый
[70, 30], // пенсионер
[-1, null], // невалидный — ожидаем ошибку
])('getDiscount(%d)', (age, expected) => {
if (expected === null) {
expect( => getDiscount(age)).toThrow;
} else {
expect(getDiscount(age)).toBe(expected);
}
});
ЭК + граничные значения вместе
Оба метода часто используются совместно: ЭК определяет классы, граничные значения уточняют тесты на стыках классов.
Частые ошибки
- Один тест на весь класс — нужно именно одно репрезентативное значение из каждого класса, не несколько похожих
- Пропуск невалидных классов — тестируют только "happy path"; система должна правильно обрабатывать неверные входные данные
- Слишком крупные классы — если внутри класса есть поведенческие различия, его нужно разделить