惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

人人都是产品经理
人人都是产品经理
博客园_首页
博客园 - 三生石上(FineUI控件)
V
Visual Studio Blog
Hugging Face - Blog
Hugging Face - Blog
美团技术团队
小众软件
小众软件
T
Tailwind CSS Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
月光博客
月光博客
有赞技术团队
有赞技术团队
WordPress大学
WordPress大学
博客园 - 【当耐特】
Apple Machine Learning Research
Apple Machine Learning Research
罗磊的独立博客
V
V2EX
酷 壳 – CoolShell
酷 壳 – CoolShell
IT之家
IT之家
量子位
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Recent Announcements
Recent Announcements
M
MIT News - Artificial intelligence
阮一峰的网络日志
阮一峰的网络日志
The GitHub Blog
The GitHub Blog

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания
Property-based тестирование: как находить баги, которые в...
Earthly Man · 2026-06-23 · via Все публикации подряд на Хабре

Средний

5 мин

0

Тесты на примерах проверяют те случаи, до которых вы додумались. Property-based тесты проверяют тысячи случаев, до которых вы НЕ додумались, — и при падении сами ужимают вход до минимального контрпримера. Разбираем, как это работает под капотом, какие свойства бывают, и показываем на коде (Python/Hypothesis), как PBT за минуту находит баг, который ручной тест-кейс не нашёл бы никогда.

Классический unit-тест — это «вход X → ожидаю Y». Вы придумали кейс, зафиксировали ожидание, поехали. Проблема в том, что баги обычно живут не в кейсах, которые вы придумали, а ровно в тех, до которых не дотянулась фантазия: пустая строка, эмодзи, дубликаты, отрицательный ноль, перевод строки внутри значения, целочисленное переполнение.

Property-based testing (PBT) переворачивает подход: вы описываете свойство — утверждение, которое должно быть истинно для любого корректного входа, — а фреймворк сам генерирует сотни случайных входов и пытается это свойство опровергнуть. Нашёл контрпример — ужимает его до минимального («shrinking») и показывает вам.

Пример на примерах vs свойство

Тест на примерах для функции сортировки:

def test_sort_example():
    assert my_sort([3, 1, 2]) == [1, 2, 3]

Один вход, одно ожидание. А вот свойства, которые верны для любого списка:

from hypothesis import given, strategies as st

@given(st.lists(st.integers()))
def test_sort_keeps_length(xs):
    assert len(my_sort(xs)) == len(xs)          # длина не меняется

@given(st.lists(st.integers()))
def test_sort_is_ordered(xs):
    r = my_sort(xs)
    assert all(r[i] <= r[i + 1] for i in range(len(r) - 1))  # реально отсортировано

@given(st.lists(st.integers()))
def test_sort_is_permutation(xs):
    assert sorted(my_sort(xs)) == sorted(xs)    # это перестановка исходного, ничего не потеряли/не добавили

@given(...) запускает каждый тест на сотне+ сгенерированных списков: пустых, из одного элемента, с дубликатами, с MIN_INT/MAX_INT, гигантских. Вам не нужно придумывать эти кейсы — генератор делает это за вас.

Как это работает под капотом

Три кирпича:

1. Генераторы (strategies) — описывают пространство входов: st.integers()st.text()st.lists(...)st.dictionaries(...), и комбинации.

2. Свойство — булево утверждение, которое должно держаться для всех входов из этого пространства.

3. Shrinking — когда свойство падает на каком-то монструозном случайном входе, фреймворк автоматически ищет минимальный вход, на котором оно всё ещё падает.

Именно shrinking превращает PBT из «рандомного фаззера» в инструмент отладки. Сравните два сообщения о падении:

# без shrinking — попробуй пойми, что тут сломалось:
Falsifying example: s = 'a8Q\x00\udce2\n\t   ZZ9📦\r\x7f...(2000 символов)'

# с shrinking — Hypothesis ужал до минимума:
Falsifying example: test_roundtrip(s='\n')

Во втором случае сразу видно: баг в обработке перевода строки. Это и есть киллер-фича.

Каталог свойств: что вообще утверждать

Самое сложное в PBT — не код, а придумать свойство. Хорошая новость: есть готовые паттерны, которые покрывают большинство случаев.

1. Round-trip (туда-обратно). Самый мощный и частый. decode(encode(x)) == x: сериализация/парсинг, кодеки, JSON, URL-энкодинг, сжатие.

@given(st.text())
def test_json_roundtrip(s):
    assert json.loads(json.dumps(s)) == s

2. Инвариант. Что-то, что всегда истинно про результат: длина, сумма, упорядоченность, «баланс не ушёл в минус», «id уникальны».

3. Идемпотентность. f(f(x)) == f(x): нормализация, slugifytrim, дедупликация, применение миграции дважды.

@given(st.text())
def test_slugify_idempotent(s):
    assert slugify(slugify(s)) == slugify(s)

4. Оракул / две реализации. Сравнить быструю реализацию с медленной-но-очевидной (или со старой версией при рефакторинге): результаты должны совпадать на любом входе.

5. Метаморфические отношения. Когда «правильный ответ» неизвестен, но известно, как он должен меняться: sort(reverse(xs)) == sort(xs); добавление товара в корзину увеличивает итог ровно на цену товара.

6. «Никогда не падает». Самое дешёвое свойство для старта: на любом валидном входе функция не кидает необработанное исключение. Часто ловит первые баги ещё до того, как вы сформулировали что-то умнее.

Реальный баг за минуту

Допустим, мы написали «наивный» CSV-сериализатор:

def to_csv_row(fields: list[str]) -> str:
    return ",".join(fields)

def from_csv_row(line: str) -> list[str]:
    return line.split(",")

Round-trip-свойство:

@given(st.lists(st.text()))
def test_csv_roundtrip(fields):
    assert from_csv_row(to_csv_row(fields)) == fields

Hypothesis почти мгновенно находит и ужимает контрпример:

Falsifying example: test_csv_roundtrip(fields=[','])

Поле, внутри которого есть запятая, ломает round-trip: [','] → "," → ['', '']. Классический баг CSV-экранирования, который на «нормальных» примерах (['a','b']) никогда бы не вылез. Вы его не придумывали — генератор придумал за вас. Это и есть ценность.

Stateful / model-based testing

Высшая лига PBT: тестировать не функцию, а последовательность операций над системой (очередь, кэш, БД, API). Вы описываете возможные действия и модель (упрощённый эталон), а фреймворк генерирует случайные сценарии и сверяет систему с моделью после каждого шага.

from hypothesis.stateful import RuleBasedStateMachine, rule, invariant

class CacheModel(RuleBasedStateMachine):
    def __init__(self):
        super().__init__()
        self.real = LruCache(capacity=3)
        self.model = {}

    @rule(k=st.integers(), v=st.integers())
    def put(self, k, v):
        self.real.put(k, v)
        self.model[k] = v

    @invariant()
    def size_within_capacity(self):
        assert len(self.real) <= 3

Так находят баги вытеснения, гонок состояний, рассинхрона — то, что обычным тестом ловится только случайно. Именно таким подходом Джон Хьюз с QuickCheck находил баги в распределённых БД и автомобильном софте — см. оригинальную статью QuickCheck (Claessen & Hughes, 2000), с которой всё началось.

Инструменты по языкам

  • Python — Hypothesis (де-факто стандарт, отличный shrinking, stateful из коробки).

  • JS/TS — fast-check.

  • JVM (Java/Kotlin) — jqwik, QuickTheories.

  • .NET — FsCheck.

  • Go — rapid, gopter.

  • Rust — proptest, quickcheck.

  • Haskell/Erlang — QuickCheck, PropEr (родина подхода).

Подводные камни — честно

  • Сформулировать свойство сложнее, чем кейс. Это главный барьер. Начинайте с «не падает» и round-trip, дальше войдёте во вкус.

  • Генератор должен покрывать реальное пространство. Если ограничить st.text() только ASCII — не найдёте багов с Unicode. И наоборот: слишком широкий генератор даёт «нереальные» падения.

  • Флак при скрытой недетерминированности. Свойство, зависящее от времени/глобального состояния/реального рандома, будет мигать. Фиксируйте время и seed.

  • Воспроизводимость. Hypothesis хранит базу упавших примеров и при следующем прогоне проверяет их первыми — коммитьте её в CI, иначе «поймал баг локально, в CI зелено».

  • Скорость. PBT-тест в сотни раз тяжелее одного юнита. Держите его в отдельном прогоне/наборе, а не в самом горячем pre-commit.

  • Это не замена примерам. Конкретный регресс-кейс на найденный баг по-прежнему полезен (быстрый, читаемый). PBT и примеры дополняют друг друга.

Как внедрить за один спринт

  • Возьмите одну «чистую» функцию с понятным контрактом: парсер, сериализатор, нормализатор, расчёт.

  • Напишите два свойства: «не падает» + round-trip (или инвариант).

  • Прогоните — почти наверняка что-то найдётся; ужатый контрпример превратите в обычный регресс-тест.

  • Закоммитьте базу примеров Hypothesis в репозиторий, добавьте PBT-набор в CI отдельным шагом.

  • Дальше добавляйте свойства на оракул (при рефакторинге — старая vs новая реализация) и, когда созреете, stateful-модель на ключевую структуру данных.

Вывод. Property-based тестирование закрывает слепое пятно тестов-на-примерах: оно systematically бьёт по входам, до которых вы не додумались, и при падении даёт минимальный, читаемый контрпример. Стоит начать с одной функции и пары свойств «не падает + round-trip» — и вы удивитесь, что найдёте уже в первый день.


Пишу про практику QA каждый день в Telegram-канале «QA — Quality Assurance»: t.me/qa10100011000001. Если зашло — забегайте.