Перейти к содержанию
Innovialabs

Property-based тестирование: Hypothesis и fast-check на практике

Продвинутое3 мин

Коротко

Вместо нескольких примеров вы описываете свойство, которое верно для любых входных данных, а Hypothesis или fast-check генерируют сотни случаев и сжимают найденную ошибку до минимального примера.

Свойства вместо примеров

Обычный тест проверяет пример: «для 2 и 3 сумма равна 5». Property-based тест проверяет утверждение, которое должно выполняться для любых входных данных: «результат сортировки упорядочен и содержит те же элементы».

Библиотека сама генерирует много входов — пустые списки, отрицательные числа, юникод, очень длинные строки — и ищет тот, на котором свойство нарушается. Это хорошо дополняет обычные тесты: примеры фиксируют известное поведение, свойства ищут то, о чём вы не подумали.

  • Python — Hypothesis.
  • JavaScript и TypeScript — fast-check.

Генераторы и shrinking

Генератор (в Hypothesis — strategy, в fast-check — arbitrary) описывает, какие данные создавать: st.integers(), st.lists(st.text()), fc.array(fc.integer()), fc.record({...}). Генераторы комбинируются, поэтому можно собрать объект заказа или пользователя.

Shrinking — главное удобство. Когда найдена ошибка на огромном случайном входе, библиотека упрощает его шаг за шагом, пока не получит минимальный пример, на котором ошибка всё ещё воспроизводится. Вместо списка из 80 случайных чисел вы видите [0, -1].

Какие свойства проверять

СвойствоПример
Round-tripdecode(encode(x)) == x, parse(format(x)) == x
Инвариантпосле сортировки длина та же, элементы упорядочены
Идемпотентностьnormalize(normalize(x)) == normalize(x)
Оракулбыстрая функция совпадает с простой медленной версией
Не падаетфункция не бросает исключение на любом валидном входе

Начинайте с round-trip и «не падает» — их проще всего сформулировать.

Hypothesis: деньги не делятся поровну

Функция делит сумму в тийинах на n частей:

from hypothesis import given, strategies as st

def split_amount(total, n):
    part = round(total / n)
    return [part] * n

@given(st.integers(min_value=0, max_value=10**9),
       st.integers(min_value=1, max_value=100))
def test_parts_sum_to_total(total, n):
    assert sum(split_amount(total, n)) == total

Примеры вроде (100, 4) проходят, но Hypothesis быстро находит нарушение и сжимает его до чего-то вроде total=1, n=2: части не складываются в исходную сумму. Исправление — распределить остаток:

def split_amount(total, n):
    q, r = divmod(total, n)
    return [q + 1] * r + [q] * (n - r)

fast-check: самодельная сериализация

Простая функция превращает объект в строку key=value&...:

import fc from "fast-check";

const stringify = (o: Record<string, string>) =>
  Object.entries(o).map(([k, v]) => `${k}=${v}`).join("&");

fc.assert(
  fc.property(fc.dictionary(fc.string(), fc.string()), (obj) => {
    expect(parse(stringify(obj))).toEqual(obj);
  })
);

fast-check находит ключ или значение со знаком & или =, после чего round-trip ломается. Ручной тест с { name: "Ali" } такую ошибку не показал бы. Исправление — экранировать значения, например через encodeURIComponent.

Как внедрять без боли

  1. Выберите чистые функции: парсеры, форматтеры, расчёты, преобразования данных.
  2. Сформулируйте одно свойство и ограничьте генератор реальным доменом (диапазоны, длины).
  3. Найденный контрпример сохраните как обычный пример-тест — это регрессия.
  4. Фиксируйте seed при падении в CI: обе библиотеки сообщают, как воспроизвести запуск.
  5. Не дублируйте реализацию в свойстве — иначе тест повторит ту же ошибку.

Типичные ошибки

  • Слишком широкие генераторы, из-за которых большинство случаев отбрасывается фильтрами.
  • Свойство, которое тривиально истинно и ничего не проверяет.
  • Медленные тесты: тяжёлые операции с базой внутри каждой итерации.

Частые вопросы

Нет, они дополняют их. Примеры документируют конкретное поведение и бизнес-правила, свойства ищут краевые случаи, которые трудно придумать вручную.

Поможем с этим

Если хотите не разбираться самим, а получить результат — этим мы занимаемся каждый день.

Обсудить проект

Читайте также

Есть идея или задача? Расскажите нам

Ответим в течение рабочего дня и предложим решение. Консультация бесплатная.