Покрытие кода тестами в TypeScript: как перестать гадать и начать измерять
Представьте, что вы собираете сложный механизм с множеством шестерёнок. Вы уверены, что собрали всё правильно, но стоит нажать кнопку «Пуск», как раздаётся скрежет и механизм заклинивает. Знакомая ситуация? В мире TypeScript-разработки такие «механизмы» — это наши приложения. А покрытие кода тестами — это контрольная сборка, которая показывает, какие детали мы проверили, а какие оставили на волю случая.
Многие разработчики относятся к метрикам покрытия скептически: «Погоня за процентами — зло», «100% покрытие не гарантирует отсутствие багов». И это правда. Но, как и любой инструмент, покрытие кода может быть полезным или вредным — всё зависит от того, как им пользоваться. В этой статье мы разберём, как правильно измерять покрытие в TypeScript, какие инструменты для этого существуют и как не превратить метрики в самоцель.
Что такое покрытие кода и почему TypeScript — не исключение
Покрытие кода (code coverage) — это метрика, показывающая, какая часть исходного кода была выполнена во время тестов. В TypeScript, который компилируется в JavaScript, измерение покрытия происходит на уровне скомпилированного кода, но современные инструменты умеют красиво отображать результаты в терминах исходного TS-кода.
Основные метрики покрытия, которые нужно знать:
- Line coverage — процент строк кода, которые были выполнены хотя бы один раз.
- Branch coverage — процент ветвлений (if/else, switch, тернарные операторы), которые были протестированы.
- Function coverage — процент функций/методов, которые были вызваны.
- Statement coverage — процент отдельных инструкций (операторов) в коде.
Для TypeScript-проектов особенно важна branch coverage, так как строгая типизация не спасает от логических ошибок в условиях. Вы можете правильно типизировать все переменные, но забыть про случай, когда массив пуст, а функция ожидает хотя бы один элемент.
Инструменты для измерения покрытия в TypeScript
1. Istanbul (nyc) — дедушка всех покрытий
Istanbul — это самый популярный инструмент для измерения покрытия JavaScript и TypeScript кода. Он работает как самостоятельная утилита (через пакет nyc) или как встроенный модуль в тестовых фреймворках.
Чтобы начать работу с Istanbul в TypeScript-проекте, установите:
npm install --save-dev nyc typescript ts-node
Создайте файл .nycrc для конфигурации:
{
"extends": "@istanbuljs/nyc-config-typescript",
"all": true,
"include": ["src/**/*.ts"],
"exclude": ["src/**/*.test.ts", "src/**/*.spec.ts"],
"reporter": ["text", "html", "lcov"],
"check-coverage": true,
"branches": 80,
"lines": 85,
"functions": 85,
"statements": 85
}
Запуск тестов с покрытием:
npx nyc mocha --require ts-node/register src/**/*.test.ts
Istanbul генерирует отчёты в консоли (text) и в виде HTML-страниц, где зелёным подсвечен покрытый код, а красным — непокрытый. Это очень наглядно.
2. Jest — всё в одном
Jest — самый популярный тестовый фреймворк для TypeScript. Он уже включает встроенный инструмент покрытия на основе Istanbul. Вам не нужно устанавливать ничего дополнительно.
Базовая настройка в jest.config.ts:
import type { Config } from 'jest';
const config: Config = {
preset: 'ts-jest',
testEnvironment: 'node',
collectCoverage: true,
coverageDirectory: 'coverage',
coverageReporters: ['text', 'html', 'lcov'],
collectCoverageFrom: [
'src/**/*.ts',
'!src/**/*.test.ts',
'!src/**/*.spec.ts',
'!src/types/**/*.ts'
],
coverageThreshold: {
global: {
branches: 80,
functions: 85,
lines: 85,
statements: 85
}
}
};
export default config;
Команда для запуска:
npx jest --coverage
Jest автоматически создаст папку coverage с HTML-отчётом. Откройте coverage/lcov-report/index.html в браузере — и вы увидите детальную карту покрытия вашего проекта.
3. Vitest — современный и быстрый
Vitest — это молодой, но мощный тестовый фреймворк, созданный для Vite-экосистемы. Он полностью совместим с Jest API, но работает в 2-3 раза быстрее за счёт нативной поддержки ESM и умного кеширования.
Настройка покрытия в vitest.config.ts:
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
coverage: {
provider: 'istanbul', // или 'c8' (ещё быстрее)
reporter: ['text', 'html', 'lcov'],
include: ['src/**/*.ts'],
exclude: ['src/**/*.test.ts', 'src/**/*.spec.ts'],
thresholds: {
branches: 80,
functions: 85,
lines: 85,
statements: 85
}
}
}
});
Vitest поддерживает два провайдера покрытия: istanbul (классика) и c8 (на основе V8, работает быстрее). Для TypeScript-проектов рекомендую использовать c8 — он точнее и быстрее.
Запуск:
npx vitest --coverage
Пример: пишем тесты и смотрим покрытие
Давайте на практике посмотрим, как работает покрытие. Создадим простой модуль для расчёта скидки:
// src/discount.ts
export function calculateDiscount(price: number, isPremium: boolean): number {
if (price <= 0) {
throw new Error('Price must be positive');
}
let discount = 0;
if (isPremium) {
discount = 0.2;
}
if (price > 1000) {
discount = Math.max(discount, 0.15);
}
return price * (1 - discount);
}
Напишем тесты (неполные, чтобы увидеть разницу в покрытии):
// src/discount.test.ts
import { calculateDiscount } from './discount';
describe('calculateDiscount', () => {
it('should apply premium discount', () => {
const result = calculateDiscount(500, true);
expect(result).toBe(400); // 500 * 0.8
});
it('should throw error for negative price', () => {
expect(() => calculateDiscount(-100, false)).toThrow('Price must be positive');
});
});
Запустим тесты с покрытием через Jest и посмотрим результат:
----------------------|---------|----------|---------|---------|-------------------
File | % St