Вы уверены, что ваши тесты действительно работают?
Представьте: у вас 95% покрытия кода, все тесты зелёные, CI/CD проходит без ошибок. Команда довольна, менеджер спокоен. Но однажды в продакшене всплывает баг. Вы открываете код, смотрите на тесты и понимаете: они просто проверяют, что функция возвращает число, но не смотрят на его значение. Или хуже — тест написан так, что проходит при любом раскладе.
Знакомая ситуация? Добро пожаловать в мир, где процент покрытия — это лишь цифра, а не гарантия качества. Здесь на сцену выходит мутационное тестирование (mutation testing) — методика, которая реально заставляет ваши тесты страдать и доказывать свою состоятельность.
В этой статье мы разберём, как внедрить mutation testing в TypeScript-проект, используя Stryker, и почему это может стать вашим новым любимым инструментом (или новым источником головной боли).
Что такое мутационное тестирование простыми словами?
Мутационное тестирование — это когда вы берёте свой код, намеренно вносите в него мелкие ошибки (мутации) и смотрите, упадут ли ваши тесты. Если тесты не падают — значит, вы тестируете неправильно. Если падают — вы молодец, но мутант уничтожен.
Звучит как садизм? Возможно. Но это работает. Представьте, что вы — QA-инженер, который тайком залез в код и заменил === на ==, > на < или убрал вызов функции. Хороший тест обязан это заметить. Плохой — пропустит.
В TypeScript, с его строгой типизацией, мутационное тестирование особенно полезно: оно ловит случаи, когда типы правильные, а логика — нет.
Почему обычное покрытие кода (code coverage) — это иллюзия?
Допустим, у вас есть функция:
function calculateDiscount(price: number, isPremium: boolean): number {
if (isPremium) {
return price * 0.8;
}
return price;
}
И тест:
test('returns number', () => {
const result = calculateDiscount(100, true);
expect(typeof result).toBe('number');
});
Покрытие? 100%. Тест? Проходит. А что, если мы заменим price * 0.8 на price * 0.5? Тест всё равно пройдёт — он же проверяет только тип, а не значение. Вот вам и ложное чувство безопасности.
Мутационное тестирование подсветит такую мутацию и скажет: «Эй, твой тест не заметил, что я изменил скидку!». И вам придётся дописать нормальную проверку.
Как работает mutation testing в TypeScript: пошагово
Весь процесс можно разбить на три этапа:
- Генерация мутантов — инструмент копирует ваш код и вносит одно изменение за раз. Например, меняет
&&на||или удаляет вызов метода. - Прогон тестов — для каждого мутанта запускаются ваши тесты.
- Анализ результатов — если тесты упали, мутант «убит». Если нет — он «выжил», и это зона риска.
Главный метрика — mutation score (процент убитых мутантов). 100% — идеал, но на практике 80-90% уже отлично.
Инструмент №1 для TypeScript: Stryker
В мире TypeScript и JavaScript король мутационного тестирования — Stryker (StrykerJS). Он быстрый, умеет работать с Jest, Vitest, Mocha и даже с браузерными тестами. Настраивается за 5 минут.
Установим:
npm install --save-dev @stryker-mutator/core @stryker-mutator/jest-runner
Создадим конфиг stryker.conf.js:
// stryker.conf.js
export default {
mutate: ['src/**/*.ts', '!src/**/*.spec.ts'], // что мутировать
testRunner: 'jest',
reporters: ['html', 'clear-text', 'progress'],
jest: {
projectType: 'custom',
configFile: 'jest.config.ts',
},
concurrency: 4, // сколько мутантов тестировать параллельно
};
Запускаем:
npx stryker run
Stryker проанализирует код, создаст мутантов и покажет отчёт в HTML. Выглядит это как карта ваших слабых мест.
Пример из реального кода: тестируем валидатор
Допустим, у нас есть простой валидатор email:
// src/validators.ts
export function isValidEmail(email: string): boolean {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
И тест:
// src/validators.spec.ts
import { isValidEmail } from './validators';
describe('isValidEmail', () => {
it('should return true for valid email', () => {
expect(isValidEmail('test@example.com')).toBe(true);
});
it('should return false for invalid email', () => {
expect(isValidEmail('invalid')).toBe(false);
});
});
Запускаем Stryker. Что он сделает? Он создаст мутантов:
- Заменит регулярку на
/^[^\s@]+@[^\s@]+\.[^\s@]+$/→ заменит на пустой regex/(?:)/ - Заменит
returnнаreturn falseилиreturn true - Удалит вызов
.test()
Если наши тесты не заметят, что функция всегда возвращает true — мутант выживет. И мы поймём: нам не хватает теста на случай, когда email — пустая строка или строка без @.
Добавим:
it('should return false for empty string', () => {
expect(isValidEmail('')).toBe(false);
});
Теперь мутант, который заменяет всё на true, будет убит. А мы — спокойнее.
Типы мутаций, которые Stryker генерирует в TypeScript
Stryker умеет делать более 30 видов мутаций. Вот самые полезные для TypeScript-разработчика:
1. Мутации операторов сравнения
===→!==>=→>instanceof→ удаление проверки
2. Логические мутации
-
Блоги
Рекомендаций по книгам
Читайте также
Go в 2025: почему язык от Google становится королём бэкенда
Деплой JavaScript (Node.js) на Kubernetes: пошаговое руководство
Clean Code на JavaScript: принципы написания чистого кода
Serverless функции на TypeScript: AWS Lambda от А до Я
Лучшие IDE и инструменты для JavaScript разработки: полный гайд