Mutation Testing в JavaScript: как поймать неуловимые баги

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Ваши тесты врут? Время мутационного тестирования

Вы пишете юнит-тесты, покрытие кода (code coverage) радует глаз — 85%, 90%, а баги всё равно вылезают на продакшене? Знакомая боль. Классическая метрика покрытия (line/branch coverage) — это, по сути, измерение того, какие строки кода были запущены. Но она ничего не говорит о том, насколько хорошо тесты проверяют логику. Они могут просто "пробегать" по строкам, не проверяя корректность результата.

Здесь на сцену выходит mutation testing (мутационное тестирование). Это как укол вакцины для вашего кода: мы намеренно вносим в программу небольшие "вирусы" (мутации), и смотрим, убивают ли их наши тесты. Если тест не падает после изменения === на !== — ваши тесты слабы. В этой статье на pythonlib.ru мы разберем, как внедрить mutation testing в JavaScript-проект, чтобы сделать ваш код по-настоящему надёжным.

Что такое мутационное тестирование простыми словами

Представьте, что вы — QA-инженер, который тайком меняет строчку в коде разработчика:

// Было
if (user.age >= 18) { allowAccess(); }

// Стало (мутация)
if (user.age > 18) { allowAccess(); }

Если после этой замены юнит-тесты всё ещё зелёные — тесты не проверяют граничное условие 18. Мутационный фреймворк делает тысячи таких замен автоматически и выдаёт отчёт: сколько мутаций выжило (survived). Чем меньше выживших мутантов — тем надёжнее ваши тесты.

Основные виды мутаций в JavaScript:

  • Изменение операторов сравнения: > → >=, === → !==
  • Удаление вызовов функций: validate() → пустая строка
  • Изменение числовых литералов: 0 → 1, 100 → 0
  • Инвертирование логических условий: if (isActive) → if (!isActive)
  • Подмена строковых значений (реже, но бывает)

Почему code coverage — это ложная безопасность

Многие разработчики на JavaScript обольщаются 100% покрытием кода. Давайте посмотрим на реальный пример:

// Функция, которая определяет скидку
function getDiscount(price, isPremium) {
    if (price > 100 && isPremium) {
        return price * 0.8;
    }
    return price;
}

И "идеальный" тест:

test('should return price for non-premium', () => {
    expect(getDiscount(50, false)).toBe(50);
});

test('should return price for premium with low price', () => {
    expect(getDiscount(50, true)).toBe(50);
});

Покрытие: 100% строк, 100% веток. Но мутация price > 100 → price >= 100 выживет! Тесты не проверяют граничное значение 100.01 или ровно 100. Mutation testing мгновенно подсветит эту "слепую зону".

Инструмент №1: Stryker Mutator для JavaScript

Самый популярный и зрелый инструмент для мутационного тестирования в мире JS — Stryker Mutator (или просто Stryker). Он поддерживает Jest, Mocha, Jasmine, Karma и работает с TypeScript.

Установка и быстрый старт

Установите Stryker глобально или локально в ваш проект:

npm install --save-dev @stryker-mutator/core @stryker-mutator/jest-runner

Создайте конфигурационный файл stryker.conf.json:

{
  "$schema": "./node_modules/@stryker-mutator/core/schema/stryker-schema.json",
  "packageManager": "npm",
  "reporters": ["html", "clear-text", "progress"],
  "testRunner": "jest",
  "coverageAnalysis": "perTest",
  "thresholds": {
    "high": 80,
    "low": 60,
    "break": 50
  }
}

Запустите мутационное тестирование:

npx stryker run

Stryker начнёт "мутировать" ваш код. На примере простого проекта с 10 юнит-тестами это может занять 1-3 минуты. Результат вы увидите в консоли и в детальном HTML-отчёте.

Пример работы Stryker на практике

Допустим, у нас есть модуль валидации email:

// validate.js
export function isValidEmail(email) {
    const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    return re.test(email);
}

И тест:

import { isValidEmail } from './validate';

test('valid email returns true', () => {
    expect(isValidEmail('test@example.com')).toBe(true);
});

test('invalid email returns false', () => {
    expect(isValidEmail('invalid')).toBe(false);
});

Stryker создаст мутанта: return !re.test(email) (инвертирование результата). Если тесты всё равно зелёные — ваш тест на false-кейс не работает (он ловит только тривиальный случай). Придётся добавить проверку на email без точки или с пробелом. Mutation testing заставляет вас писать более осмысленные тесты.

Продвинутые техники: как не утонуть в мутантах

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

  • Инкрементальный запуск: Используйте флаг --incremental, чтобы Stryker перепроверял только изменённые файлы.
  • Фильтрация по файлам: npx stryker run --mutate src/**/*.js !src/legacy/** — исключите легаси-код.
  • Тайм-ауты: Мутанты, вызывающие бесконечные циклы, будут убиты по тайм-ауту. Это нормально.
  • Thresholds (пороги): Установите "break": 60 — если процент выживших мутантов выше 40%, CI упадёт.

Интеграция с CI/CD (GitHub Actions)

Добавьте mutation testing в пайплайн. Пример шага в GitHub Actions:

- name: Run mutation tests
  run: npx stryker run --thresholds.break=70
  env:
    STRYKER_DASHBOARD_API_KEY: ${{ secrets.STRYKER_API_KEY }}

Теперь каждый PR будет проверяться на "живучесть" мутантов. Это дисциплини

Блоги

Рекомендаций по книгам