Покрытие кода через @vitest/coverage-v8 и istanbul

Покрытие кода показывает, какие части приложения были выполнены во время тестирования, а какие остались непроверенными. В экосистеме Vite и Vitest поддерживаются два основных механизма покрытия:

  • @vitest/coverage-v8
  • @vitest/coverage-istanbul

Оба варианта интегрируются напрямую в тестовый раннер Vitest и позволяют получать отчёты о покрытии в различных форматах.

Покрытие используется для:

  • контроля качества тестов;
  • поиска непроверенных веток логики;
  • анализа мёртвого кода;
  • настройки CI/CD;
  • контроля минимальных порогов покрытия;
  • генерации HTML-отчётов.

Архитектура покрытия в Vitest

Vitest отделяет механизм запуска тестов от механизма анализа покрытия. Для этого используются отдельные провайдеры.

Основные провайдеры:

Провайдер Пакет Основа
V8 @vitest/coverage-v8 встроенный profiler движка V8
Istanbul @vitest/coverage-istanbul инструментирование кода

Настройка производится через параметр coverage.provider.

Пример:

export default defineConfig({
  test: {
    coverage: {
      provider: 'v8'
    }
  }
})

или:

export default defineConfig({
  test: {
    coverage: {
      provider: 'istanbul'
    }
  }
})

Установка @vitest/coverage-v8

Установка пакета

npm install -D vitest @vitest/coverage-v8

Для pnpm:

pnpm add -D vitest @vitest/coverage-v8

Для yarn:

yarn add -D vitest @vitest/coverage-v8

Базовая конфигурация

Файл vite.config.ts:

import { defineConfig } from 'vite'

export default defineConfig({
  test: {
    coverage: {
      provider: 'v8',
      reporter: ['text', 'html']
    }
  }
})

Запуск покрытия

Через CLI:

vitest run --coverage

или:

npx vitest --coverage

Генерация HTML-отчёта

После выполнения тестов появляется каталог:

coverage/

Внутри:

coverage/index.html

HTML-отчёт содержит:

  • процент покрытия;
  • список файлов;
  • покрытие строк;
  • покрытие веток;
  • покрытие функций;
  • подсветку непокрытого кода.

Как работает coverage-v8

Использование встроенного механизма V8

@vitest/coverage-v8 использует встроенный profiler JavaScript-движка V8.

Преимущества такого подхода:

  • высокая скорость;
  • минимальное замедление тестов;
  • отсутствие предварительного инструментирования;
  • низкое потребление памяти.

В отличие от Istanbul, код не модифицируется перед выполнением.


Особенности V8-покрытия

Скорость

V8-покрытие обычно быстрее Istanbul, особенно в крупных проектах.

Меньше искажений кода

Так как код не переписывается, сохраняются:

  • оригинальные sourcemaps;
  • корректные stack traces;
  • нативное выполнение модулей.

Хорошая интеграция с ESM

V8 особенно эффективен в проектах Vite благодаря нативной работе с ESM.


Ограничения V8

Несмотря на производительность, существуют ограничения:

  • менее точное покрытие некоторых edge-case конструкций;
  • возможные различия при работе с TypeScript sourcemaps;
  • нестабильность некоторых branch coverage сценариев;
  • зависимость от возможностей движка Node.js.

Установка Istanbul-провайдера

Установка

npm install -D @vitest/coverage-istanbul

Конфигурация

import { defineConfig } from 'vite'

export default defineConfig({
  test: {
    coverage: {
      provider: 'istanbul',
      reporter: ['text', 'html']
    }
  }
})

Принцип работы Istanbul

Инструментирование кода

Istanbul работает иначе:

  1. исходный код анализируется;
  2. в него внедряются счётчики;
  3. тесты выполняют модифицированный код;
  4. счётчики фиксируют выполнение строк и веток.

Пример концептуального инструментирования:

Исходный код:

function sum(a, b) {
  return a + b
}

После инструментирования:

function sum(a, b) {
  coverage.counter++
  return a + b
}

Преимущества Istanbul

Более точное покрытие

Istanbul традиционно считается эталоном branch coverage.

Особенно хорошо отслеживаются:

  • условные ветки;
  • switch;
  • ternary operators;
  • сложные логические выражения.

Зрелая экосистема

Istanbul существует давно и поддерживается многими инструментами:

  • Jest;
  • Babel;
  • nyc;
  • SonarQube;
  • Codecov.

Гибкость отчётов

Поддерживаются:

  • LCOV;
  • Cobertura;
  • JSON;
  • Clover;
  • HTML;
  • text-summary.

Недостатки Istanbul

Более низкая скорость

Инструментирование создаёт дополнительную нагрузку.

В крупных проектах разница может быть существенной.


Усложнение sourcemaps

Изменение исходного кода иногда приводит к:

  • смещению строк;
  • сложностям отладки;
  • менее читаемым stack traces.

Сравнение V8 и Istanbul

Характеристика V8 Istanbul
Скорость высокая ниже
Инструментирование нет да
Точность branch coverage хорошая очень высокая
Потребление памяти ниже выше
Работа с sourcemaps проще сложнее
Подходит для больших проектов отлично зависит от размера
Совместимость с legacy-инструментами средняя высокая

Настройка reporters

Text reporter

coverage: {
  reporter: ['text']
}

Вывод в терминал:

File        | % Stmts | % Branch | % Funcs | % Lines

HTML reporter

coverage: {
  reporter: ['html']
}

Создаёт визуальный интерфейс отчёта.


LCOV reporter

coverage: {
  reporter: ['lcov']
}

Используется CI-сервисами:

  • Codecov;
  • Coveralls;
  • SonarQube.

Несколько репортёров

coverage: {
  reporter: [
    'text',
    'html',
    'lcov'
  ]
}

Включение и исключение файлов

include

coverage: {
  include: ['src/**/*.ts']
}

exclude

coverage: {
  exclude: [
    'node_modules/',
    'dist/',
    'coverage/',
    '**/*.d.ts'
  ]
}

Исключение тестовых файлов

coverage: {
  exclude: [
    'tests/',
    '**/*.test.ts'
  ]
}

all: true

Покрытие даже неиспользуемых файлов

По умолчанию покрываются только файлы, загруженные во время тестирования.

Параметр:

all: true

заставляет анализировать все файлы проекта.

Пример:

coverage: {
  all: true,
  include: ['src/**/*.ts']
}

Зачем нужен all

Без all: true файл без тестов может вообще не попасть в отчёт.

С all: true такой файл покажет:

0% coverage

Это позволяет видеть реальные пробелы тестирования.


Thresholds

Минимальные пороги покрытия

Vitest умеет падать при недостаточном покрытии.

Пример:

coverage: {
  thresholds: {
    lines: 80,
    functions: 80,
    branches: 70,
    statements: 80
  }
}

Проверка в CI

Если покрытие ниже порога:

Exit code 1

Это особенно важно для:

  • GitHub Actions;
  • GitLab CI;
  • Jenkins;
  • Azure Pipelines.

Проверка branch coverage

Что такое branch coverage

Branch coverage анализирует выполнение всех логических ветвей.

Пример:

function getRole(user) {
  if (user.isAdmin) {
    return 'admin'
  }

  return 'user'
}

Если протестирован только isAdmin = true, branch coverage будет неполным.


Пример полного покрытия

import { describe, it, expect } from 'vitest'

function getRole(user) {
  if (user.isAdmin) {
    return 'admin'
  }

  return 'user'
}

describe('getRole', () => {
  it('admin', () => {
    expect(
      getRole({ isAdmin: true })
    ).toBe('admin')
  })

  it('user', () => {
    expect(
      getRole({ isAdmin: false })
    ).toBe('user')
  })
})

Игнорирование покрытия

Игнорирование строк

Для Istanbul:

/* istanbul ignore next */
if (import.meta.env.DEV) {
  debug()
}

Игнорирование файлов

/* istanbul ignore file */

Игнорирование V8

V8 использует совместимые pragma-комментарии:

/* v8 ignore next */

Работа с TypeScript

Поддержка sourcemaps

Vitest корректно работает с TypeScript через:

  • esbuild;
  • Vite transform pipeline;
  • sourcemaps.

Особенности transpilation

Некоторые конструкции TypeScript преобразуются в дополнительный JavaScript-код.

Например:

enum Status {
  Active,
  Disabled
}

После компиляции появляется дополнительная логика, которая тоже может участвовать в покрытии.


Рекомендации

Для TypeScript-проектов обычно рекомендуется:

  • включать sourcemaps;
  • использовать современные target;
  • избегать лишней трансформации Babel;
  • использовать Node.js актуальной версии.

Покрытие Vue-проектов

Покрытие SFC-компонентов

Vitest умеет анализировать:

.vue

файлы через Vite pipeline.


Пример

<script setup lang="ts">
const visible = true
</script>

<template>
  <div v-if="visible">
    Content
  </div>
</template>

Условие v-if участвует в branch coverage.


Покрытие React-компонентов

JSX и TSX

Vitest корректно покрывает:

  • JSX;
  • TSX;
  • hooks;
  • functional components.

Пример

type Props = {
  loading: boolean
}

export function Button(props: Props) {
  if (props.loading) {
    return <span>Loading</span>
  }

  return <button>Send</button>
}

Для полного покрытия нужны тесты обеих веток.


Coverage directory

Настройка директории

coverage: {
  reportsDirectory: './custom-coverage'
}

Очистка отчётов

coverage: {
  clean: true
}

Отключение очистки

coverage: {
  clean: false
}

Полезно при объединении нескольких запусков.


Coverage в монорепозиториях

Отдельные отчёты

Каждый пакет может иметь:

packages/app
packages/ui
packages/core

с собственным coverage.


Общий отчёт

Можно объединять LCOV-файлы через внешние инструменты:

  • nyc;
  • lcov-result-merger;
  • cobertura merge utilities.

Использование coverage в CI/CD

GitHub Actions

Пример:

- name: Run tests
  run: npm run test -- --coverage

Загрузка в Codecov

- name: Upload coverage
  uses: codecov/codecov-action@v4

SonarQube

Используется LCOV:

coverage: {
  reporter: ['lcov']
}

Файл:

coverage/lcov.info

Производительность покрытия

Когда использовать V8

coverage-v8 особенно подходит:

  • для больших Vite-проектов;
  • быстрых CI pipeline;
  • monorepo;
  • активной разработки;
  • ESM-first архитектуры.

Когда использовать Istanbul

istanbul предпочтителен:

  • при строгом анализе branch coverage;
  • интеграции со старой инфраструктурой;
  • использовании legacy tooling;
  • сложной аналитике покрытия.

Практическая конфигурация для V8

import { defineConfig } from 'vite'

export default defineConfig({
  test: {
    coverage: {
      provider: 'v8',

      all: true,

      include: [
        'src/**/*.ts',
        'src/**/*.tsx'
      ],

      exclude: [
        'node_modules/',
        'dist/',
        'coverage/',
        '**/*.test.ts',
        '**/*.spec.ts'
      ],

      reporter: [
        'text',
        'html',
        'lcov'
      ],

      thresholds: {
        lines: 80,
        functions: 80,
        branches: 70,
        statements: 80
      }
    }
  }
})

Практическая конфигурация для Istanbul

import { defineConfig } from 'vite'

export default defineConfig({
  test: {
    coverage: {
      provider: 'istanbul',

      all: true,

      reporter: [
        'text',
        'html',
        'lcov'
      ],

      thresholds: {
        lines: 85,
        branches: 80
      }
    }
  }
})

Типичные проблемы

Coverage показывает 0%

Причины:

  • файл не импортируется;
  • отсутствует all: true;
  • include/exclude настроены неверно;
  • тесты не запускаются.

Неправильные строки в отчёте

Причины:

  • broken sourcemaps;
  • Babel-transform;
  • старый TypeScript target;
  • конфликт плагинов.

Медленная генерация coverage

Причины:

  • Istanbul в крупном проекте;
  • слишком широкие include-маски;
  • coverage для node_modules;
  • тяжёлые integration tests.

Рекомендации по выбору

coverage-v8

Подходит для:

  • большинства Vite-проектов;
  • frontend-разработки;
  • React;
  • Vue;
  • Svelte;
  • быстрых CI.

istanbul

Подходит для:

  • enterprise-инфраструктуры;
  • legacy-проектов;
  • глубокого branch coverage;
  • совместимости с существующими системами аналитики.

Рекомендуемый современный стек

Для большинства современных Vite-проектов оптимальным вариантом считается:

Vitest + @vitest/coverage-v8

Причины:

  • высокая скорость;
  • простая настройка;
  • нативная интеграция с Vite;
  • хорошая поддержка ESM;
  • минимальные накладные расходы;
  • корректная работа с современным frontend-стеком.