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 })

Связанные темы

Ресурсы