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

推荐订阅源

大猫的无限游戏
大猫的无限游戏
Security Archives - TechRepublic
Security Archives - TechRepublic
T
Threat Research - Cisco Blogs
A
Arctic Wolf
S
Securelist
O
OpenAI News
T
Threatpost
Forbes - Security
Forbes - Security
N
News and Events Feed by Topic
S
Secure Thoughts
H
Heimdal Security Blog
S
Security Affairs
P
Privacy International News Feed
C
Cisco Blogs
C
CERT Recently Published Vulnerability Notes
Cyberwarzone
Cyberwarzone
N
News and Events Feed by Topic
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
P
Palo Alto Networks Blog
S
Security @ Cisco Blogs
Hacker News - Newest:
Hacker News - Newest: "LLM"
博客园 - 三生石上(FineUI控件)
月光博客
月光博客
T
Tailwind CSS Blog
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Hacker News: Ask HN
Hacker News: Ask HN
T
Troy Hunt's Blog
S
SegmentFault 最新的问题
腾讯CDC
V
Visual Studio Blog
Last Week in AI
Last Week in AI
H
Hacker News: Front Page
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Project Zero
Project Zero
WordPress大学
WordPress大学
NISL@THU
NISL@THU
博客园 - 【当耐特】
博客园 - Franky
Webroot Blog
Webroot Blog
博客园_首页
T
Tenable Blog
雷峰网
雷峰网
Google Online Security Blog
Google Online Security Blog
阮一峰的网络日志
阮一峰的网络日志
V2EX - 技术
V2EX - 技术
Recent Commits to openclaw:main
Recent Commits to openclaw:main
L
Lohrmann on Cybersecurity
The Hacker News
The Hacker News

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

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет 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 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Property-based testing на практике: как находить баги, о которых вы не подумали
artem · 2026-06-16 · via Все публикации подряд на Хабре

Средний

8 мин

5

Вы написали тридцать юнит-тестов, подобрали входы руками, всё зелёное, релиз уезжает. Через неделю прод падает на пустой строке, на отрицательном нуле, на числе чуть больше int32, на юникоде с суррогатными парами или на списке с дубликатами. Тест на это не сработал по простой причине: пример-ориентированный тест проверяет ровно те случаи, которые вы придумали, а баги живут в тех, которые не придумали.

Property-based testing может изменить подход. Вместо «на входе X получаю Y» вы формулируете свойство, истинное для любого входа, а фреймворк сам генерирует сотни разнообразных значений и пытается это свойство сломать.

В статье разберём на Hypothesis для Python, как писать такие свойства, какие они бывают, как описывать вход, как тестировать системы с состоянием и где этот подход бьёт обычные тесты, а где нет.

Первый property-тест

Самое наглядное свойство — round-trip: если данные закодировать, а потом раскодировать, должно получиться исходное. Обычный тест проверил бы пару строк, property-тест проверяет любые.

from hypothesis import given, strategies as st

@given(st.text())
def test_roundtrip(s):
    assert decode(encode(s)) == s

Hypothesis сгенерирует много значений s, и в их числе те, о которых руками не вспоминают: пустая строка, одиночный перевод строки, эмодзи, символы из разных юникод-плоскостей, суррогатные пары. Если хоть одно сломает равенство, тест упадёт и сообщит конкретное значение. Никакого списка примеров вы не пишете — пишете утверждение, истинное для всего домена.

Зелёный тест и баг, которого он не видит

Покажем разницу на конкретной функции. Пусть chunk режет список на куски по n элементов, и в реализации есть распространённая ошибка — теряется последний неполный кусок.

def chunk(xs, n):
    out = []
    for i in range(0, len(xs), n):
        if i + n <= len(xs):          # баг: неполный хвост в результат не попадает
            out.append(xs[i:i + n])
    return out

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

def test_chunk_example():
    assert chunk([1, 2, 3, 4], 2) == [[1, 2], [3, 4]]   # зелёный

Property-тест формулирует то, что должно держаться всегда: склейка кусков обратно даёт исходный список, и ни один кусок не длиннее n.

@given(st.lists(st.integers()), st.integers(min_value=1, max_value=10))
def test_chunk_reassembles(xs, n):
    chunks = chunk(xs, n)
    assert [x for c in chunks for x in c] == xs         # склейка == исходный список
    assert all(len(c) <= n for c in chunks)

Hypothesis быстро находит падение, плюсом минимизирует его до простейшего вида — например, xs=[0], n=2: функция вернула пустой список, склейка дала [], а ожидался [0]. Минимальный контрпример сразу указывает на природу бага — потерю хвоста, а не тонет в случайном списке из сотен элементов.

Какие свойства вообще бывают

Главная работа в property-тестировании — придумать свойство. Есть несколько типовых форм, и почти любая функция попадает хотя бы в одну.

  • Инверсия и round-trip — когда у операции есть обратная: сериализация и парсинг, сжатие и распаковка, кодирование и декодирование, сохранение и загрузка. Свойство: прямая, затем обратная, возвращают исходное.

  • Идемпотентность — когда повторное применение ничего не меняет: нормализация, сортировка, дедупликация, приведение к канону.

@given(st.lists(st.integers()))
def test_sort_invariants(xs):
    out = sorted(xs)
    assert len(out) == len(xs)                          # длина сохранена
    assert all(a <= b for a, b in zip(out, out[1:]))    # результат упорядочен
    assert sorted(out) == out                           # идемпотентность
  • Инвариант — свойство, которое держится для любого выхода независимо от входа: сортировка сохраняет длину и мультимножество элементов, маршрут не длиннее суммы рёбер, сумма по счетам неизменна. Эталон (oracle) — когда есть медленная, но заведомо верная реализация: новую быструю проверяют против неё, или новую версию против старой.

@given(st.lists(st.integers()))
def test_against_reference(xs):
    assert my_fast_sort(xs) == sorted(xs)               # эталон — встроенная сортировка
  • Метаморфические соотношения — связь между выходами на связанных входах, когда правильного ответа вы не знаете: добавление элемента увеличивает счётчик ровно на единицу, перестановка входа не меняет сумму, удвоение всех цен удваивает итог.

Несколько свойств на одной функции

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

intervals = st.lists(
    st.tuples(st.integers(0, 100), st.integers(0, 100)).map(lambda t: (min(t), max(t)))
)

@given(intervals)
def test_merge_properties(ivs):
    out = merge_intervals(ivs)

    assert all(a[1] < b[0] for a, b in zip(out, out[1:]))   # упорядочены и не пересекаются
    assert merge_intervals(out) == out                       # идемпотентность

    def covered(point, segs):
        return any(s <= point <= e for s, e in segs)
    for s, e in ivs:
        assert covered(s, out) and covered(e, out)           # покрытие входа сохранено

Стратегия intervals через .map гарантирует, что начало отрезка не больше конца, так что генератор не тратит время на заведомо невалидные пары. Три свойства — порядок, идемпотентность и сохранение покрытия — вместе фиксируют контракт функции, и любая реализация, которая теряет интервал, переставляет границы или оставляет пересечение, провалит хотя бы одно.

Стратегии: как описывать вход

Вход задаётся стратегиями — описаниями того, какие значения генерировать. Базовые покрывают примитивы и контейнеры, доменные объекты собираются из них, а сложные структуры — рекурсией.

ages = st.integers(min_value=0, max_value=120)
emails = st.from_regex(r"[a-z]{1,8}@[a-z]{1,8}\.[a-z]{2,4}", fullmatch=True)
users = st.builds(User, name=st.text(min_size=1, max_size=20), age=ages, email=emails)

colors = st.sampled_from(["red", "green", "blue"])      # из фиксированного множества
maybe_int = st.none() | st.integers()                    # объединение вариантов

st.floats стоит настраивать под задачу: по умолчанию он генерирует nan и бесконечности, и если код к ним не готов, либо обрабатывайте их, либо отключайте через allow_nan=False, allow_infinity=False. Для древовидных и JSON-подобных данных есть рекурсивные стратегии:

json_value = st.recursive(
    st.none() | st.booleans() | st.integers() | st.text(),
    lambda children: st.lists(children) | st.dictionaries(st.text(), children),
    max_leaves=20,
)

Когда нужно построить значение с зависимостью между частями, помогает @st.composite, где можно рисовать значения последовательно:

@st.composite
def sorted_pair(draw):
    a = draw(st.integers())
    b = draw(st.integers(min_value=a))      # второй элемент по построению не меньше первого
    return (a, b)

А если решение, что рисовать дальше, зависит от уже сгенерированного прямо внутри теста, используется st.data:

@given(st.data())
def test_index_in_list(data):
    xs = data.draw(st.lists(st.integers(), min_size=1))
    i = data.draw(st.integers(0, len(xs) - 1))   # индекс зависит от длины уже нарисованного списка
    assert xs[i] in xs

Для ограничений входа есть два пути — assume, отбрасывающий неподходящее значение, и конструирование валидного значения через map/builds. Конструирование почти всегда лучше.

@given(st.integers())
def test_division(x):
    assume(x != 0)                  # редкое исключение можно отбросить, частое — лучше не генерировать
    assert (10 // x) * x + 10 % x == 10

Стейтфул-тестирование: проверка системы против модели

Еще одна сила Hypothesis — генерация не одного входа, а целых последовательностей операций. Так тестируют объекты с состоянием: кэш, очередь, хранилище, конечный автомат. Вы описываете операции правилами, рядом держите простую эталонную модель, и Hypothesis генерирует случайные сценарии вызовов, проверяя, что реальная система ведёт себя как модель.

from hypothesis.stateful import RuleBasedStateMachine, rule, invariant

class KVStateMachine(RuleBasedStateMachine):
    def __init__(self):
        super().__init__()
        self.model = {}                 # эталон — обычный dict
        self.store = KVStore()          # тестируемая реализация

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

    @rule(k=st.integers(0, 50))
    def check(self, k):
        assert self.store.get(k) == self.model.get(k)

    @invariant()
    def size_matches(self):
        assert self.store.size() == len(self.model)

TestKV = KVStateMachine.TestCase

Hypothesis перебирает случайные цепочки put и check, после каждого шага сверяя состояние, а инвариант проверяется на каждом шаге автоматически. Такой тест ловит ошибки, возникающие только на определённой последовательности операций, именно их пример-ориентированные тесты пропускают чаще всего.

Когда правила создают сущности, на которые ссылаются последующие операции (открытые соединения, дескрипторы, созданные записи), их складывают в Bundle и берут оттуда как вход других правил. Для систем с вытеснением, вроде LRU-кэша, в модель закладывают и политику вытеснения, иначе эталон разойдётся с реализацией после первого выселения.

Целенаправленная генерация

Иногда баг прячется не в случайном входе, а в экстремальном — самом медленном, самом большом, с максимальным расхождением. Чистая случайная генерация натыкается на такие входы редко. target подсказывает Hypothesis, какую величину максимизировать, и поиск смещается в сторону тяжёлых случаев.

from hypothesis import given, target

@given(st.lists(st.integers()))
def test_runtime_bounded(xs):
    elapsed = measure_runtime(xs)
    target(elapsed)              # двигаем генерацию к входам, максимизирующим время
    assert elapsed < LIMIT

Это превращает property-тест в лёгкий инструмент поиска худшего случая — деградации производительности, переполнения, патологического входа для алгоритма с плохой асимптотикой.

Чего стоит избегать

Property-тесты дают ложную уверенность, если свойство выбрано неудачно, и тут есть несколько ошибок.

  • Первая — переписать реализацию в свойстве: если проверять функцию против её же повторной реализации, вы написали один и тот же код дважды, и общий баг тест пропустит. Спасают независимый эталон и структурные инварианты, не повторяющие логику.

  • Вторая — недетерминированное свойство, зависящее от времени, случайности или порядка обхода множества: оно будет мигать и подрывать доверие к набору тестов. Свойство должно быть детерминированным при фиксированном входе.

  • Третья — тривиально истинное свойство, которое держится независимо от наличия бага; проверить его осмысленность можно, временно внеся ошибку в функцию и убедившись, что тест краснеет.

Воспроизводимость и база контрпримеров

Когда тест падает, Hypothesis сохраняет найденный контрпример в локальную базу и при следующем запуске сначала проигрывает сохранённые падения. Один раз пойманный баг автоматически становится регрессионным тестом и не исчезает, пока не починен. Конкретный случай можно прибить к тесту явно через @example, а полную детерминированность дать через @seed.

from hypothesis import given, example

@given(st.text())
@example("")            # пустую строку проверяем всегда, не полагаясь на случай
@example("\x00")        # и нулевой байт
def test_parse(s):
    assert parse(s) is not None or s == ""

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

Производительность и health-checks

Каждый property-тест прогоняет функцию много раз, поэтому стоит следить за бюджетом. Число примеров задаётся через settings(max_examples=...), ограничение времени на один пример — через deadline. Health-checks предупреждают о проблемах генерации: слишком медленная функция, слишком много отброшенных фильтром значений, слишком тяжёлые данные. Срабатывание health-check — сигнал поправить стратегию, а не отключить проверку: чаще всего за ним стоит фильтрация там, где нужно конструирование, или генерация данных большего размера, чем требуется.

from hypothesis import given, settings

@settings(max_examples=500, deadline=200)    # 500 примеров, не дольше 200 мс на каждый
@given(st.lists(st.integers(), max_size=100))
def test_pipeline(xs):
    assert process(xs) is not None

В CI обычно держат умеренный max_examples, а более долгий прогон с большим числом примеров и расширенной генерацией выносят в отдельный ночной запуск, где время не критично.

В итоге

Property-based testing не заменяет пример-ориентированные тесты, а закрывает то пространство входов, которое вы не продумали: вместо перечисления случаев вы утверждаете свойство, истинное для всего домена, и фреймворк сам ищет минимальный контрпример.

Основная работа — выбрать свойство: инверсию, идемпотентность, инвариант, эталон или метаморфическое соотношение, а для совсем тяжёлых случаев достаточно проверки «не падает». Вход описывается стратегиями, и валидный домен лучше конструировать через builds, map и composite, чем выфильтровывать через assume.

Продолжить тему автоматизированного поиска ошибок можно на бесплатных открытых уроках OTUS. Преподаватели-практики покажут рабочие инструменты, разберут подходы на примерах и ответят на вопросы по ходу занятия.

  • 2 июля, 20:00. «REST Assured & JSON Schema Validator: автоматизация тестирования API на практике». Записаться

  • 23 июля, 20:00. «Фаззинг и реверс: как понять, что делает программа, найти в ней ошибки». Записаться

Больше бесплатных уроков смотрите в дайджесте.