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

Плагин Rollup представляет собой программный модуль, который вмешивается в различные этапы сборки: разрешение путей, загрузку файлов, трансформацию кода, генерацию чанков, выпуск ресурсов и другие процессы. Ошибка внутри плагина способна привести к повреждению выходного кода, нарушению tree shaking, появлению некорректных sourcemap или полной остановке сборки.

По этой причине тестирование является обязательной частью разработки плагинов. Даже относительно простой плагин может содержать десятки сценариев поведения:

  • обработка различных типов файлов;
  • работа с относительными и абсолютными путями;
  • взаимодействие с другими плагинами;
  • корректная генерация ошибок;
  • поддержка нескольких форматов вывода;
  • сохранение совместимости между версиями Rollup.

Качественный набор тестов позволяет безопасно расширять функциональность и предотвращать регрессии.


Уровни тестирования плагинов

Обычно тестирование делится на несколько уровней.

Модульные тесты

Проверяют отдельные функции без запуска Rollup.

Например, если плагин содержит функцию преобразования путей:

export function normalizePath(path) {
    return path.replace(/\\/g, '/');
}

Тест может выглядеть следующим образом:

import { expect, test } from 'vitest';
import { normalizePath } from '../src/utils.js';

test('normalizes windows path', () => {
    expect(
        normalizePath('src\\components\\app.js')
    ).toBe('src/components/app.js');
});

Подобные тесты выполняются быстро и позволяют проверять бизнес-логику независимо от Rollup.


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

Проверяют работу плагина внутри настоящего процесса сборки.

Именно такие тесты являются основными для Rollup-плагинов.

Проверяется:

  • вызов хуков;
  • корректность трансформаций;
  • структура выходных файлов;
  • работа sourcemap;
  • взаимодействие с API Rollup.

Сквозные тесты

Воспроизводят реальный сценарий использования.

Создается небольшой тестовый проект:

fixture/
├── src/
│   ├── main.js
│   └── utils.js
└── rollup.config.js

После сборки анализируется итоговый результат.

Такие тесты максимально приближены к реальным условиям эксплуатации.


Выбор инструмента тестирования

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

  • Vitest;
  • Jest;
  • Mocha;
  • Node.js Test Runner.

Особенно популярным вариантом является Vitest.

Установка:

npm install -D vitest

Минимальный тест:

import { test, expect } from 'vitest';

test('sum', () => {
    expect(2 + 2).toBe(4);
});

Использование JavaScript API Rollup

Основная стратегия тестирования заключается в программном запуске Rollup через API.

Вместо выполнения команды:

rollup -c

сборка запускается напрямую:

import { rollup } from 'rollup';

Пример:

const bundle = await rollup({
    input: 'src/main.js',
    plugins: []
});

После завершения можно анализировать результат.


Создание интеграционного теста

Пусть имеется плагин:

export default function bannerPlugin() {
    return {
        name: 'banner',

        transform(code) {
            return `/* banner */\n${code}`;
        }
    };
}

Тест:

import { test, expect } from 'vitest';
import { rollup } from 'rollup';
import bannerPlugin from '../src/plugin.js';

test('adds banner', async () => {
    const bundle = await rollup({
        input: 'fixtures/input.js',
        plugins: [bannerPlugin()]
    });

    const result = await bundle.generate({
        format: 'esm'
    });

    const code = result.output[0].code;

    expect(code.startsWith('/* banner */')).toBe(true);
});

Тест выполняет настоящую сборку и проверяет результат.


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

Некоторые плагины создают файлы.

Например:

this.emitFile({
    type: 'asset',
    fileName: 'manifest.json',
    source: '{}'
});

Для проверки таких сценариев удобно использовать временные директории.

Node.js предоставляет:

import os from 'node:os';
import fs from 'node:fs/promises';
import path from 'node:path';

Создание каталога:

const tempDir = await fs.mkdtemp(
    path.join(os.tmpdir(), 'rollup-test-')
);

После завершения тестов каталог удаляется.


Проверка содержимого выходных файлов

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

Пример:

const output = await bundle.generate({
    format: 'esm'
});

const chunk = output.output[0];

expect(chunk.code).toContain('console.log');

Проверка имени файла:

expect(chunk.fileName).toBe('main.js');

Проверка экспортов:

expect(chunk.exports).toContain('myFunction');

Тестирование хука resolveId

Один из наиболее важных хуков.

Плагин:

function virtualPlugin() {
    return {
        name: 'virtual',

        resolveId(id) {
            if (id === 'virtual:data') {
                return id;
            }
        }
    };
}

Тест:

test('resolves virtual module', async () => {
    const plugin = virtualPlugin();

    const result = plugin.resolveId('virtual:data');

    expect(result).toBe('virtual:data');
});

Это пример модульного тестирования.

Интеграционный вариант проверяет полноценную сборку.


Тестирование хука load

Плагин:

function virtualPlugin() {
    return {
        name: 'virtual',

        load(id) {
            if (id === 'virtual:data') {
                return 'export default 123';
            }
        }
    };
}

Проверка:

expect(
    plugin.load('virtual:data')
).toContain('123');

Либо через полноценную сборку и анализ экспорта.


Тестирование хука transform

Наиболее распространенный тип тестов.

Плагин:

transform(code) {
    return code.replace(
        '__VERSION__',
        '"1.0.0"'
    );
}

Тест:

expect(
    transformedCode
).toContain('"1.0.0"');

Дополнительно проверяются:

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

Проверка sourcemap

Если плагин возвращает карту исходников:

return {
    code,
    map
};

необходимо убедиться, что карта существует.

Пример:

const result = plugin.transform(
    sourceCode,
    'file.js'
);

expect(result.map).toBeDefined();

При интеграционном тестировании:

const output = await bundle.generate({
    sourcemap: true
});

Далее:

expect(
    output.output[0].map
).toBeDefined();

Проверка emitFile

Плагин:

this.emitFile({
    type: 'asset',
    fileName: 'data.json',
    source: '{}'
});

После сборки:

const asset = output.output.find(
    item => item.type === 'asset'
);

Проверка:

expect(asset.fileName)
    .toBe('data.json');

Проверка содержимого:

expect(asset.source)
    .toBe('{}');

Проверка предупреждений

Плагин может создавать предупреждения.

Пример:

this.warn('deprecated option');

Для тестирования удобно перехватывать обработчик предупреждений:

const warnings = [];

await rollup({
    input,
    plugins,
    onwarn(warning) {
        warnings.push(warning);
    }
});

Проверка:

expect(warnings.length)
    .toBe(1);

Содержимое:

expect(warnings[0].message)
    .toContain('deprecated');

Проверка ошибок

Плагин:

this.error('invalid configuration');

Тест:

await expect(
    rollup(config)
).rejects.toThrow(
    'invalid configuration'
);

Проверка ошибок особенно важна для нестандартных сценариев.


Тестирование виртуальных модулей

Многие плагины используют виртуальные модули.

Пример:

resolveId(id) {
    if (id === 'virtual:env') {
        return '\0virtual:env';
    }
}

и

load(id) {
    if (id === '\0virtual:env') {
        return 'export const mode = "test"';
    }
}

Тест должен проверять:

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

Тестирование нескольких форматов вывода

Плагин может работать по-разному в зависимости от формата.

Проверка ESM:

await bundle.generate({
    format: 'esm'
});

Проверка CommonJS:

await bundle.generate({
    format: 'cjs'
});

Проверка IIFE:

await bundle.generate({
    format: 'iife',
    name: 'App'
});

Каждый формат желательно тестировать отдельно.


Тестирование взаимодействия плагинов

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

Пример:

plugins: [
    pluginA(),
    pluginB()
]

Необходимо проверить:

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

Использование fixture-проектов

Крупные плагины обычно содержат каталог fixtures.

Структура:

test/
├── fixtures/
│   ├── basic
│   ├── css
│   ├── virtual
│   ├── assets
│   └── dynamic-import
└── plugin.test.js

Каждая папка представляет отдельный сценарий.

Пример:

fixtures/
└── css
    ├── input.js
    └── style.css

Тест запускает сборку конкретного fixture и сравнивает результат с ожидаемым.


Snapshot-тестирование

Полезно для крупных трансформаций.

Полученный код сравнивается со снимком.

Пример:

expect(code)
    .toMatchSnapshot();

При первом запуске создается файл:

__snapshots__

Последующие тесты сравнивают новый результат со старым.

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

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

Недостаток заключается в том, что чрезмерное использование snapshot-тестов затрудняет понимание причин ошибок.


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

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

Пример:

const start = performance.now();

await rollup(config);

const end = performance.now();

Проверка:

expect(end - start)
    .toBeLessThan(1000);

Подобные тесты позволяют выявлять деградацию производительности.


Непрерывная интеграция

Тесты обычно запускаются автоматически через CI-системы.

Типичный процесс:

npm run lint
npm run test
npm run build

Наиболее распространённые платформы:

  • GitHub Actions;
  • GitLab CI/CD;
  • Jenkins;
  • CircleCI.

Автоматический запуск тестов после каждого изменения позволяет обнаруживать проблемы до публикации новой версии плагина.


Рекомендации по организации тестов

Хорошая тестовая база обычно обладает следующими характеристиками:

  • каждый тест проверяет один сценарий;
  • фикстуры изолированы друг от друга;
  • отсутствуют зависимости между тестами;
  • используются реальные сборки через Rollup API;
  • проверяются ошибки и предупреждения;
  • тестируются все поддерживаемые форматы;
  • отдельно контролируется работа sourcemap;
  • покрываются крайние случаи и некорректный ввод;
  • важные трансформации дополнительно защищаются snapshot-тестами;
  • выполняется автоматический запуск тестов в CI.

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