Юнит-тестирование плагинов

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

Плагины для Remark и Rehype строятся вокруг концепции обработки абстрактного синтаксического дерева (AST). Remark работает с Markdown AST (MDAST), а Rehype — с HTML AST (HAST). Каждый плагин — это функция, которая принимает дерево и модифицирует его, добавляет узлы, удаляет или преобразует существующие.

Простейший шаблон плагина Remark выглядит так:

function myRemarkPlugin(options = {}) {
  return (tree, file) => {
    // tree — MDAST
    // file — объект VFile, содержит метаданные и содержимое
  };
}

Для Rehype структура аналогична, но дерево уже HAST:

function myRehypePlugin(options = {}) {
  return (tree, file) => {
    // tree — HAST
    // file — объект VFile
  };
}

Выбор подхода к юнит-тестированию

Юнит-тесты плагинов сосредотачиваются на проверке корректности преобразований AST. Основные стратегии:

  1. Сравнение исходного и ожидаемого AST После применения плагина создается дерево, которое сравнивается с заранее подготовленным эталонным AST. Этот метод хорошо подходит для точных трансформаций, где структура узлов критична.

  2. Сравнение результирующего Markdown/HTML После применения плагина AST преобразуется обратно в Markdown или HTML и сравнивается с ожидаемым текстом. Этот подход удобен, когда важен конечный результат, а не структура дерева.

  3. Снапшоты Фреймворки вроде Jest позволяют сохранять снимки AST или HTML и автоматически проверять изменения. Удобно для крупных плагинов с множеством вариантов преобразований.

Инструменты для тестирования

  • unified — ядро для создания конвейеров обработки, необходимо для запуска плагинов в тестах.
  • remark / rehype — библиотеки для парсинга и генерации Markdown/HTML.
  • vfile — представляет виртуальные файлы с содержимым, метаданными и сообщениями об ошибках.
  • jest или vitest — фреймворки для написания тестов и выполнения ассертов.

Пример теста плагина Remark

Плагин добавляет в конец документа заголовок:

function addFooterHeader() {
  return (tree) => {
    const headingNode = {
      type: 'heading',
      depth: 2,
      children: [{ type: 'text', value: 'Footer' }]
    };
    tree.children.push(headingNode);
  };
}

// Тестирование с использованием Jest
import {unified} from 'unified';
import remarkParse from 'remark-parse';
import remarkStringify from 'remark-stringify';

test('добавление заголовка в конец документа', async () => {
  const processor = unified()
    .use(remarkParse)
    .use(addFooterHeader)
    .use(remarkStringify);

  const input = '## Title\n\nSome text.';
  const output = String(await processor.process(input));

  expect(output).toContain('## Footer');
});

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

Тестирование Rehype плагинов

Для Rehype аналогичный подход, но с HTML:

function addClassToParagraphs() {
  return (tree) => {
    visit(tree, 'element', (node) => {
      if (node.tagName === 'p') {
        node.properties = node.properties || {};
        node.properties.className = ['highlight'];
      }
    });
  };
}

// Тест с использованием unified и rehype
import {unified} from 'unified';
import rehypeParse from 'rehype-parse';
import rehypeStringify from 'rehype-stringify';
import {visit} from 'unist-util-visit';

test('добавление класса ко всем параграфам', async () => {
  const processor = unified()
    .use(rehypeParse, {fragment: true})
    .use(addClassToParagraphs)
    .use(rehypeStringify);

  const input = '<p>Hello</p><p>World</p>';
  const output = String(await processor.process(input));

  expect(output).toContain('<p class="highlight">Hello</p>');
  expect(output).toContain('<p class="highlight">World</p>');
});

Работа с асинхронными плагинами

Некоторые плагины выполняют асинхронные операции (загрузка внешних данных, API-запросы). Для них важно использовать async/await и корректно тестировать обработку:

function asyncPlugin() {
  return async (tree) => {
    await new Promise(resolve => setTimeout(resolve, 10));
    tree.children.push({ type: 'text', value: 'Async content' });
  };
}

test('асинхронное добавление контента', async () => {
  const processor = unified().use(asyncPlugin);
  const tree = { type: 'root', children: [] };
  await processor.run(tree);
  expect(tree.children[0].value).toBe('Async content');
});

Советы по надежному тестированию

  • Всегда создавать изолированные AST для тестов, чтобы исключить побочные эффекты.
  • Проверять крайние случаи: пустой документ, документ с нестандартными узлами, вложенные структуры.
  • Использовать унифицированный API для запуска плагина вне полного конвейера, чтобы ускорить юнит-тесты.
  • Для больших плагинов разделять тесты по функциональности: преобразование текста, модификация метаданных, обработка ошибок.
  • При сравнении AST применять функции глубокого сравнения, такие как expect(tree).toEqual(expectedTree), чтобы учесть все свойства узлов.

Отслеживание ошибок и сообщений

Плагины могут добавлять сообщения об ошибках через объект VFile. Тестирование таких сообщений позволяет убедиться, что плагин корректно реагирует на неправильный вход:

function errorPlugin() {
  return (tree, file) => {
    if (!tree.children.length) {
      file.message('Документ пустой');
    }
  };
}

test('сообщение об ошибке при пустом дереве', () => {
  const file = { messages: [] };
  errorPlugin()({ type: 'root', children: [] }, file);
  expect(file.messages[0].reason).toBe('Документ пустой');
});

Заключение по подходам

Эффективное юнит-тестирование плагинов Remark и Rehype строится на тщательной проверке работы с AST, контроле преобразований и обработке ошибок. Использование синхронного и асинхронного API unified позволяет создавать быстрые, надежные тесты, которые покрывают все ключевые сценарии работы плагина.