Покрытие кода тестами

Особенности тестирования библиотек с DOM-ориентированной архитектурой

Библиотека выбора элементов в интерфейсе работает поверх DOM и активно взаимодействует с событиями, состоянием элементов и динамическим рендерингом. Это формирует специфические требования к тестированию: значительная часть логики не является чистыми функциями, а зависит от окружения браузера.

Ключевые сложности:

  • динамическое создание и удаление DOM-узлов;
  • асинхронные операции (например, remote search);
  • обработка пользовательских событий (input, click, keydown);
  • состояние компонента, распределённое между DOM и внутренними структурами;
  • плагины, расширяющие поведение базового компонента.

Покрытие кода в таких условиях требует комбинирования unit, integration и end-to-end подходов.


Инструменты измерения покрытия

На практике используется инструмент экосистемы Istanbul:

  • nyc — CLI-обёртка для Istanbul;

  • встроенные средства покрытия в Jest;

  • поддержка в Vitest через c8;

  • отчёты форматов:

    • text
    • lcov
    • html

Пример базовой конфигурации Jest с покрытием:

export default {
  testEnvironment: "jsdom",
  collectCoverage: true,
  coverageDirectory: "coverage",
  coverageReporters: ["text", "lcov", "html"],
  collectCoverageFrom: [
    "src/**/*.js",
    "!src/**/*.d.ts",
    "!src/**/*.config.js"
  ]
};

Важный аспект: для DOM-библиотек обязательно использовать jsdom или аналогичную среду, иначе покрытие будет формально высоким, но тесты не будут отражать реальное поведение.


Структура тестируемой логики

Для эффективного покрытия важно разделить функциональность на уровни:

1. Ядро (core logic)

  • управление состоянием (items, options, selected)
  • фильтрация и поиск
  • обработка данных

2. DOM-слой

  • рендеринг dropdown
  • синхронизация состояния и интерфейса
  • управление фокусом

3. Событийная система

  • обработчики input/keydown/click
  • кастомные события (change, item_add, item_remove)

4. Плагины

  • расширение поведения (например, createItem, remove_button)
  • интеграция с core lifecycle

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


Unit-тестирование ядра

Unit-тесты должны изолировать бизнес-логику от DOM.

Пример тестирования фильтрации:

import TomSelect from "tom-select";

test("filters options correctly", () => {
  const instance = new TomSelect(document.createElement("select"), {
    options: [
      { value: "1", text: "Apple" },
      { value: "2", text: "Banana" },
      { value: "3", text: "Orange" }
    ],
    create: false
  });

  const result = instance.search("ap");

  expect(result.items.length).toBe(1);
  expect(result.items[0].text).toBe("Apple");
});

Здесь важно:

  • не тестировать DOM;
  • проверять чистую логику поиска;
  • изолировать входные данные.

Интеграционные тесты с DOM

Интеграционные тесты проверяют взаимодействие ядра и интерфейса.

Пример с jsdom:

import TomSelect from "tom-select";

beforeEach(() => {
  document.body.innerHTML = `
    <select id="test">
      <option value="1">Apple</option>
      <option value="2">Banana</option>
    </select>
  `;
});

test("opens dropdown on focus", () => {
  const el = document.querySelector("#test");

  const select = new TomSelect(el, {});

  select.focus();
  select.open();

  const dropdown = document.querySelector(".ts-dropdown");
  expect(dropdown).not.toBeNull();
});

Интеграционный уровень обязателен, поскольку значительная часть логики реализована через DOM-состояние.


Тестирование событий

События — ключевая часть поведения селекта.

Типовые сценарии:

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

Пример:

test("triggers item_add event", () => {
  const el = document.createElement("select");

  const select = new TomSelect(el, {});

  const handler = jest.fn();
  select.on("item_add", handler);

  select.addItem("1");

  expect(handler).toHaveBeenCalledWith("1");
});

Здесь важно проверять:

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

Асинхронные сценарии и remote loading

Многие конфигурации используют загрузку данных через API.

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

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

Решение — использование mock fetch:

global.fetch = jest.fn(() =>
  Promise.resolve({
    json: () =>
      Promise.resolve([
        { value: "1", text: "Apple" },
        { value: "2", text: "Banana" }
      ])
  })
);

Тест:

test("loads remote options", async () => {
  const el = document.createElement("select");

  const select = new TomSelect(el, {
    load: (query, callback) => {
      fetch("/api?q=" + query)
        .then(res => res.json())
        .then(callback);
    }
  });

  await select.load("a");

  expect(select.options["1"]).toBeDefined();
});

Асинхронные тесты обязательно должны учитывать:

  • await;
  • ожидание обновления состояния;
  • стабилизацию DOM.

Покрытие плагинов

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

Стратегия:

  • тестировать каждый плагин изолированно;
  • проверять интеграцию с core;
  • проверять совместимость нескольких плагинов.

Пример:

test("plugin creates remove button", () => {
  const el = document.createElement("select");

  const select = new TomSelect(el, {
    plugins: ["remove_button"]
  });

  select.addItem("1");

  const button = document.querySelector(".remove");
  expect(button).not.toBeNull();
});

Измерение покрытия и интерпретация метрик

Основные метрики:

  • Statements — покрытие операторов;
  • Branches — покрытие ветвлений;
  • Functions — покрытие функций;
  • Lines — покрытие строк.

Команда для запуска:

npx jest --coverage

Критический момент: высокий процент покрытия не гарантирует качество тестов.

Типичная проблема:

  • тестируется DOM-обёртка;
  • не тестируются edge cases;
  • отсутствует проверка асинхронных веток.

Настройка порогов покрытия

Для устойчивого проекта задаются минимальные пороги:

coverageThreshold: {
  global: {
    statements: 80,
    branches: 70,
    functions: 80,
    lines: 80
  }
}

Порог ветвлений особенно важен для библиотек с большим количеством условий (например, обработка input/selection states).


Типичные ошибки при тестировании

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

Практика устойчивого покрытия

Подход, который даёт стабильное качество тестов:

  • разделение core и UI-слоя;
  • минимизация прямого доступа к DOM в unit-тестах;
  • использование integration-тестов как основного уровня для UI;
  • обязательное мокирование сети;
  • контроль событий через spies;
  • регулярный анализ branch coverage, а не только statement coverage.