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

推荐订阅源

S
Securelist
O
OpenAI News
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
T
Threat Research - Cisco Blogs
D
Darknet – Hacking Tools, Hacker News & Cyber Security
Google Online Security Blog
Google Online Security Blog
C
CXSECURITY Database RSS Feed - CXSecurity.com
N
News and Events Feed by Topic
S
Security Affairs
SecWiki News
SecWiki News
Project Zero
Project Zero
L
Lohrmann on Cybersecurity
P
Proofpoint News Feed
P
Palo Alto Networks Blog
L
LINUX DO - 最新话题
H
Hacker News: Front Page
Recent Commits to openclaw:main
Recent Commits to openclaw:main
I
Intezer
Simon Willison's Weblog
Simon Willison's Weblog
W
WeLiveSecurity
T
The Exploit Database - CXSecurity.com
K
Kaspersky official blog
The GitHub Blog
The GitHub Blog
I
InfoQ
云风的 BLOG
云风的 BLOG
雷峰网
雷峰网
B
Blog
IT之家
IT之家
AWS News Blog
AWS News Blog
Jina AI
Jina AI
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Google DeepMind News
Google DeepMind News
Spread Privacy
Spread Privacy
N
News and Events Feed by Topic
Security Latest
Security Latest
美团技术团队
C
Check Point Blog
WordPress大学
WordPress大学
T
Tenable Blog
S
Security @ Cisco Blogs
Last Week in AI
Last Week in AI
博客园 - 聂微东
月光博客
月光博客
博客园 - 【当耐特】
S
Schneier on Security
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
S
Secure Thoughts
Schneier on Security
Schneier on Security
C
Cisco Blogs
Cyberwarzone
Cyberwarzone

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

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Кто решает судьбу вашего проекта? Разбираем заинтересованные стороны. BABOK #1 Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче 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 миллионов точек без потерь
Ложное чувство защиты: Почему 90% code coverage не спасает от багов
Andrey_Biryukov · 2026-06-13 · via Все публикации подряд на Хабре

Средний

11 мин

4.2K

Привет, Хабр! Меня зовут Андрей Бирюков. Я — независимый эксперт в области ИТ и ИБ, преподаю в учебных центрах и пишу статьи и книги. Проблема, о которой мы будем говорить сегодня знакома многим тестировщикам.

Jenkins или GitLab CI рапортует: «Покрытие кода тестами — 94%». Зелёная галочка, начальство довольно, команда идёт пить кофе. А на проде через две недели падает критическая фича, потому что кто-то поменял знак сравнения с > на >=, и все тесты… всё равно прошли зелёными.

Добро пожаловать в самую дорогую иллюзию в разработке ПО: уверенность, основанную на покрытии строк. Покрытие в 90% — это как бронежилет из фольги. Он выглядит убедительно на расстоянии, но первая же пуля покажет его настоящую природу. В этой статье мы разберём три конкретные метрики покрытия, их слепые зоны и инструмент, который безжалостно покажет дыры в ваших тестах — мутационное тестирование.

Три типа покрытия: почему строки — самая бесполезная метрика

Разработчики обычно знают только один вид покрытия — сколько строк кода выполнилось во время тестов. Это всё равно, что оценивать здоровье человека по тому, как часто он моргает. Давайте на реальном куске кода увидим разницу между тремя типами покрытия.

public class PaymentProcessor {
    public String processPayment(double amount, String currency, boolean isCorporate) {
        String result = "pending";

        if (amount <= 0) {
            result = "invalid_amount";
            logError("Negative or zero amount: " + amount);
            return result;
        }

        if (currency == null || currency.isEmpty()) {
            result = "invalid_currency";
            return result;
        }

        if (isCorporate) {
            // Применяем корпоративный дисконт, но только для валюты USD
            if ("USD".equals(currency)) {
                amount = amount * 0.95;
                result = "corporate_discount_applied";
            } else {
                result = "corporate_not_supported_currency";
                return result;
            }
        }

        // Основная логика списания (опустим для примера)
        if (amount > 10000) {
            result = "requires_approval";
        } else {
            result = "approved";
        }

        return result;
    }

    private void logError(String message) {
        // логирование
    }
}

Line Coverage (покрытие строк)

Этот подход, пожалуй является самым распространенным и самым наивным. Инструмент типа JaCoCo или coverage.py просто отмечает, какие строки кода были выполнены хотя бы один раз.

Напишем тест, который даст нам высокое покрытие:

@Test
void testHappyPath() {
    PaymentProcessor processor = new PaymentProcessor();
    String result = processor.processPayment(100.0, "USD", false);
    assertEquals("approved", result);
}

 Что же в итоге здесь выполняется? Строки: объявление result, проверка amount (мимо, amount > 0), проверка currency (прошли), пропустили блок isCorporate, попали в блок else для amount > 10000 (нет, 100 <= 10000). Выполнились строки 3, 5, 9, 11, 28, 30, 32. Грубо говоря, 7 из 32 строк.

Но если мы напишем тест, который проходит все ветки по верхам, покрытие может подняться до 80-90%, при этом критические ошибки останутся незамеченными. Почему? Потому что line coverage не различает, как именно выполнена строка. Строка с условием if (a && b) считается выполненной, даже если в неё зашли только по одному из двух путей.

Branch Coverage (покрытие ветвей)

Это уже более интересный способ. Branch coverage считает не строки, а логические ветвления — каждый if, каждый switch, каждый тернарный оператор. Для каждого условия инструмент проверяет, были ли выполнены оба исхода (true и false).

В нашем примере с PaymentProcessor есть несколько ветвлений:

1. amount <= 0 — два исхода

2. currency == null || currency.isEmpty() — два исхода (но здесь два условия в одном if — некоторые инструменты считают их отдельно)

3. isCorporate — два исхода

4. Внутри isCorporate: "USD".equals(currency) — два исхода

5. amount > 10000 — два исхода

Чтобы достичь 100% покрытия, вам нужно минимум 5 тестов, покрывающих разные комбинации. Но и это не даст полной защиты. Почему? Потому что branch coverage не заботится о последовательности операций и о том, как значения комбинируются между ветками.

Mutation Coverage (мутационное покрытие)

А вот эти мутанты позволяют проверить ваш код по-настоящему. Вы не просто измеряете, что выполнилось, вы активно проверяете, способны ли ваши тесты обнаружить изменения в логике.

Идея проста и гениальна: мутационный тест-раннер берёт ваш код, создаёт тысячи его копий (мутантов), в каждой копии вносит одно маленькое изменение (мутацию), а затем прогоняет ваши тесты. Если тесты падают — мутант убит. Если тесты проходят — мутант выжил, и у вас дыра в тестовом наборе.

Мутационное тестирование на практике: PITest

Для реализаци мутационного тестирования в разных языках есть свои решения. В Java-мире стандарт — PITest, для Python — mutmut или Cosmic Ray, для JavaScript — Stryker. Механика везде будет похожей.

Установим PITest для Maven:

<plugin>
    <groupId>org.pitest</groupId>
    <artifactId>pitest-maven</artifactId>
    <version>1.15.0</version>
    <configuration>
        <targetClasses>
            <param>com.example.payment.*</param>
        </targetClasses>
        <targetTests>
            <param>com.example.payment.*</param>
        </targetTests>
        <mutators>
            <mutator>ALL</mutator>
        </mutators>
        <outputFormats>
            <format>HTML</format>
        </outputFormats>
    </configuration>
</plugin>

Для запуска выполним: mvn org.pitest:pitest-maven:mutationCoverage

Типы мутаций, которые разоблачают ваши тесты

PITest применяет десятки различных типов мутаций. Вот самые показательные:

Мутации условий (Conditional Boundary Mutator)

Исходный код: if (amount > 10000)

Мутации:

- if (amount >= 10000) — сдвиг границы

- if (amount < 10000) — инверсия

- if (amount > 9999) — арифметический сдвиг

Пример: у вас есть тест, который проверяет порог в 10000. Он передаёт amount = 10001 и ждёт "requires_approval". Тест проходит. Но что, если мутант изменил условие на >=? Ваш тест с 10001 всё равно пройдёт, потому что 10001 >= 10000 тоже true. Вы не поймали мутанта. Ошибка: ваш тест не проверяет поведение ровно на границе.

Для исправления добавьте тест с amount = 10000. До мутации должно быть "approved" (если <= 10000), после мутации (>= 10000) должно стать "requires_approval". Тест, который проверяет и то, и другое, убьёт мутанта.

Мутации возвращаемых значений (Return Values Mutator)

Исходный код: return result;

Мутация: return null; или return ""; (для строк) или return 0; (для чисел)

Ужасающая мутация. Если у вас нет теста, который явно проверяет не-null и не-пустоту возвращаемого значения в разных сценариях, мутант выживает. Ваш тест просто сравнивает с ожидаемой строкой, но если обе строки пустые — тест пройдёт.

Мутации инкрементов (Increments Mutator)

Исходный код: counter++ или counter += 1

Мутация: counter-- или counter -= 1

Классический случай. Цикл, который должен выполниться 10 раз, выполняется 8 или 12 раз. Ваш тест проверяет финальное состояние, но не количество итераций? Мутант выживает.

Мутации математических операторов (Math Mutator)

Исходный код: result = a + b;

Мутации:

- result = a - b;

- result = a * b;

- result = a / b;

- result = a % b;

 Это самые коварные мутации, потому что тесты часто проверяют только "не ноль" или "положительное число". Мутант с заменой + на - может дать тот же знак результата (например, 5+3 = 8, 5-3 = 2 — оба положительные, оба не ноль). Тест, который проверяет точное значение, убьёт мутанта. Тест, который проверяет result > 0 — нет.

Мутации констант (Constant Mutator)

Исходный код: if (statusCode == 200)

Мутации:

- if (statusCode == 201)

- if (statusCode == 199)

- if (statusCode == 0)

Эти мутации особенно опасны для HTTP-клиентов и парсеров. Если у вас есть тест с моком, который возвращает 200, а не реальным вызовом, вы никогда не узнаете, что ваш код сломается на реальном 201.

Полный цикл убийства мутанта

Давайте проследим, как выглядит настоящий тест, способный убить мутанта в нашем PaymentProcessor.

Возьмём конкретную мутацию: PITest изменяет amount <= 0 на amount < 0. Теперь отрицательный ноль (0.0) не считается невалидным. Ваш тест:

@Test
void testNegativeAmountShouldBeInvalid() {
    PaymentProcessor processor = new PaymentProcessor();
    String result = processor.processPayment(-10.0, "USD", false);
    assertEquals("invalid_amount", result);
}

Этот тест убьёт мутанта? Нет. Потому что -10.0 меньше 0, и при исходном условии (amount <= 0) и при мутированном (amount < 0) условие срабатывает. Результат одинаков. Мутант выжил.

Что нужно? Тест с amount = 0.0:

@Test
void testZeroAmountShouldBeInvalid() {
    PaymentProcessor processor = new PaymentProcessor();
    String result = processor.processPayment(0.0, "USD", false);
    assertEquals("invalid_amount", result);
}

@Test
void testNegativeAmountShouldBeInvalid() {
    PaymentProcessor processor = new PaymentProcessor();
    String result = processor.processPayment(-0.01, "USD", false);
    assertEquals("invalid_amount", result);
}

Теперь:

  • Исходный код (<=0): оба теста проходят, оба возвращают "invalid_amount".

  • Мутированный код (<0): первый тест с 0.0 не заходит в условие (0.0 не < 0) и идёт дальше, возвращая что-то другое. Второй тест с -0.01 проходит. Первый тест падает, соответственно, мутант убит.

Как читать отчёт мутационного тестирования

После прогона PITest вы получите HTML-отчёт с цветовой индикацией:

Зелёные строки — мутанты убиты. Ваши тесты хороши здесь.

Красные строки — мутанты выжили. Здесь ваши тесты не видят разницы между правильной логикой и сломанной.

Жёлтые строки — мутанты не были созданы (например, код никогда не выполняется в принципе — мёртвый код).

Процент мутационного покрытия считается просто:

Mutation Score = (Убитые мутанты) / (Всего мутантов - Нежизнеспособные) 100%

Практический порог, к которому стоит стремиться в критических модулях: 80-85% mutation coverage. Достичь 100% почти невозможно (и часто бессмысленно) из-за мутаций в логировании, getter/setter и инфраструктурном коде.

Но есть важный нюанс: мутационное тестирование медленное. PITest для модуля в 5000 строк может работать 5-10 минут. Поэтому его обычно запускают:

  • Ночью на CI для критических модулей

  • Принудительно перед релизом

  • Только для кода, который изменился в PR (инкрементальный режим)

Давайте рассмотрим реальный пример со следующими результатами: 90% line coverage, 60% branch coverage, 30% mutation coverage.

Вот код из production-системы, который проходил все проверки по line coverage, но упал в боевом окружении:

def calculate_discount(customer_tier, purchase_amount, is_first_purchase):

    discount = 0.0

    if customer_tier == "gold":
        discount = 0.15

    elif customer_tier == "silver":
        discount = 0.10

    elif customer_tier == "bronze":
        discount = 0.05

    if is_first_purchase:
        discount = discount + 0.10

    if purchase_amount > 500:
        discount = discount + 0.05

    # Логическая ошибка: не проверяется, что скидка не больше 30%
    return discount

Line coverage: 100% (все строки выполнились хотя бы в одном тесте).

Branch coverage: 85% (проверили gold/silver/bronze, с флагом и без, с суммой меньше и больше 500).

Мутационный тест находит проблему:

Мутация 1: discount = discount + 0.10discount = discount - 0.10 (инверсия знака). Выжил. Почему? Потому что ни один тест не проверял, что скидка при первом заказе увеличивается, а не уменьшается. Все тесты просто проверяли, что discount стал больше нуля.

Мутация 2: if purchase_amount > 500if purchase_amount >= 500 (сдвиг границы). Выжил. Никто не тестировал сумму ровно 500.

Мутация 3: Отсутствие ограничения 30% — не мутация, а архитектурная проблема. Но если бы ограничение было, мутация, удаляющая его, тоже выжила бы.

Итог: код упал на проде, когда gold-клиент с первым заказом на 1000 получил скидку 0.15 + 0.10 + 0.05 = 30%, а должна была быть 25% по бизнес-правилу.

Стратегия внедрения: с чего начать в понедельник

Вам не нужно срочно бежать и прогонять PITest на всём монолите. Вы завалите CI и демотивируете команду. Здесь нужно действовать постепенно:

Шаг 1. Выберите один модуль для пилота.

Кандидат: модуль с бизнес-логикой (не CRUD, не DTO). Где есть условия, расчёты, конвертации. Где баг стоит дорого.

Шаг 2. Запустите мутационный тест и посмотрите на выживших мутантов.**

Скорее всего, их будет много. Не пугайтесь. Сортируйте по типам:

  • Мутации в toString(), hashCode(), equals() — часто можно игнорировать или поставить в исключения.

  • Мутации в граничных условиях — критичны, добавляйте тесты.

  • Мутации в арифметике — критичны, добавляйте тесты с конкретными числами.

  • Мутации в константах статусов (200, 404, 500) — важны для кода, работающего с внешними API.

Шаг 3. Добавьте тесты, которые убивают хотя бы самых опасных мутантов.

Не стремитесь к 100% mutation coverage сразу. Добавьте 5-10 тестов, которые убивают мутантов в самой важной ветке бизнес-логики. Увидите, как упадёт количество false positive.

Шаг 4. Добавьте мутационный тест в ночной CI для этого модуля.

# GitLab CI example
mutation-test:
  stage: verify
  script:
    - mvn org.pitest:pitest-maven:mutationCoverage
  artifacts:
    reports:
      coverage_report:
        coverage_format: cobertura
        path: target/site/pitest/index.html
  only:
    - schedules
    - main

Шаг 5. Введите правило для новых PR.

Любой новый код, изменяющий условия (if/switch) или арифметику, должен сопровождаться тестами, которые убивают соответствующих мутантов. Проверять можно либо локальным прогоном PITest на изменённых файлах, либо инкрементальным плагином.

Итог

Мораль сегодняшний статьи можно коротко выразить следующей фразой: покройте логику, а не строки. Метрика code coverage по строкам — это индикатор того, что вы вообще запускаете тесты. Не больше. Вы можете иметь 100% line coverage и 0% mutation coverage, если ваши тесты проверяют только то, что код не падает с исключением, но не проверяют, что он делает правильные вещи.

Branch coverage уже лучше — он заставляет вас думать о разных исходах условий.

Mutation coverage — это единственная метрика, которая отвечает на правильный вопрос: «А заметят ли мои тесты, если кто-то (или я сам) случайно сломает логику?».

Помните: зелёная полоса в CI означает только то, что тесты выполнились. Она не означает, что ваш код корректен. И уж точно не означает, что вы можете спать спокойно.

Мутационное тестирование — это как прививка от самоуверенности. Она немного болит, она требует времени, но она спасает от гораздо более серьёзной болезни под названием «прод в огне, а тесты зелёные».

Больше о тестировании читайте в материалах:

Качество тестов определяется не только покрытием, но и тем, какие сценарии они действительно способны проверить. Продолжить разбираться в автоматизации тестирования, применении ИИ и метриках качества можно на бесплатных уроках OTUS:

  • 16 июня, 20:00. «ИИ в автотестах: помощник или угроза?» Записаться

  • 18 июня, 20:00. «Тесты, которые чинят себя сами: практика ИИ в UI-тестировании». Записаться

  • 2 июля, 20:00. «Как читать баги: метрики для руководителей команд тестирования (QA Lead)». Записаться

Полный список бесплатных уроков июня смотрите в дайджесте.