Property-Based Testing
Property-Based Testing — подход к тестированию, при котором вместо конкретных примеров задаются свойства (инварианты) функции, а инструмент автоматически генерирует сотни случайных входных данных для их проверки.
Зачем нужно
Обычные example-based тесты проверяют только те случаи, которые придумал разработчик. PBT автоматически ищет edge cases: пустые строки, нулевые значения, отрицательные числа, Unicode-символы. Один property-тест заменяет десятки ручных примеров и находит баги, которые разработчик не предвидел.
Где используется
- Чистые функции с математическими инвариантами (сортировка, парсинг, кодирование)
- Сериализация/десериализация:
parse(serialize(x)) === x - Алгоритмы, где легко задать свойства результата
- Тестирование граничных случаев в валидации
Основной контент
Концепция: свойства vs примеры
// Example-based — проверяем конкретные данные
test('sum(1, 2) === 3', () => expect(sum(1, 2)).toBe(3));
test('sum(0, 0) === 0', () => expect(sum(0, 0)).toBe(0));
// Property-based — проверяем инвариант для любых данных
// "сложение коммутативно: a + b === b + a"
// для любых a и b из произвольного набора
fast-check — PBT библиотека для JS/TS
npm install --save-dev fast-check
import fc from 'fast-check';
import { sum } from './math';
test('сложение коммутативно', () => {
fc.assert(
fc.property(fc.integer, fc.integer, (a, b) => {
return sum(a, b) === sum(b, a);
})
);
});
test('сложение ассоциативно', () => {
fc.assert(
fc.property(fc.integer, fc.integer, fc.integer, (a, b, c) => {
return sum(sum(a, b), c) === sum(a, sum(b, c));
})
);
});
Примеры свойств
import fc from 'fast-check';
import { sortArray } from './sort';
// Свойство 1: длина не изменяется
test('сортировка не теряет элементы', () => {
fc.assert(
fc.property(fc.array(fc.integer), (arr) => {
return sortArray(arr).length === arr.length;
})
);
});
// Свойство 2: результат отсортирован
test('результат сортировки упорядочен', () => {
fc.assert(
fc.property(fc.array(fc.integer), (arr) => {
const sorted = sortArray(arr);
return sorted.every((v, i) => i === 0 || sorted[i - 1] <= v);
})
);
});
// Свойство 3: идемпотентность
test('двойная сортировка === одна сортировка', () => {
fc.assert(
fc.property(fc.array(fc.integer), (arr) => {
return JSON.stringify(sortArray(sortArray(arr))) ===
JSON.stringify(sortArray(arr));
})
);
});
Shrinking — минимизация провального примера
При нахождении ошибки fast-check автоматически уменьшает входные данные до минимального воспроизводящего примера:
Property failed after 1 tests
{ seed: 42, path: "0:1:0", endOnFailure: true }
Counterexample: [-2147483648] ← минимальный пример
Генераторы данных
fc.integer // целое число
fc.float // float
fc.string // произвольная строка
fc.emailAddress // email
fc.array(fc.string) // массив строк
fc.object // произвольный объект
fc.record({ name: fc.string, age: fc.integer({ min: 0, max: 120 }) })
Частые ошибки
- Property слишком слабое —
sum(a, b) >= 0не тестирует корректность суммирования; ищи более сильные инварианты - Использование вместо, а не рядом с example-тестами — PBT дополняет, а не заменяет обычные тесты на конкретных примерах
- Нет seed при нестабильном тесте — фиксируй seed чтобы воспроизвести провальный пример:
fc.assert(prop, { seed: 42 })