Webpack-сборка в интеграционных тестах

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

  • объединение модулей;
  • транспиляция;
  • минификация;
  • обработка ресурсов;
  • внедрение полифилов;
  • генерация чанков;
  • внедрение runtime-кода;
  • оптимизация зависимостей.

Даже если отдельные модули успешно проходят unit-тестирование, ошибки могут появляться именно после сборки. Интеграционные тесты позволяют обнаруживать проблемы, связанные с:

  • некорректной конфигурацией загрузчиков;
  • конфликтами Babel и TypeScript;
  • ошибками tree shaking;
  • неправильной генерацией чанков;
  • несовместимостью polyfill-механизмов;
  • динамическими импортами;
  • SSR-сборкой;
  • alias-конфигурацией;
  • ошибками source maps;
  • различиями production и development сборок.

Webpack-сборка в интеграционных тестах становится полноценным объектом тестирования.


Архитектура тестирования Webpack-сборки

Типичная архитектура интеграционного тестирования включает несколько уровней:

Тестирование результата сборки

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

  • успешность компиляции;
  • отсутствие ошибок и warning;
  • структура output-файлов;
  • содержимое bundle;
  • наличие чанков;
  • генерация manifest-файлов.

Тестирование поведения собранного приложения

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

  • запускается браузер;
  • подключается bundle;
  • выполняются пользовательские сценарии;
  • анализируется runtime-поведение.

Тестирование инфраструктуры сборки

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

  • корректность plugins;
  • совместимость loaders;
  • работа asset pipeline;
  • кеширование;
  • incremental rebuild;
  • HMR-механизмы.

Изолированная тестовая сборка

Интеграционные тесты редко используют production-конфигурацию напрямую. Обычно создаётся отдельный webpack-конфиг.

Пример структуры проекта

project/
├─ src/
├─ tests/
│  ├─ integration/
│  ├─ fixtures/
│  └─ webpack/
├─ webpack.config.js
├─ webpack.test.config.js

Создание тестового webpack-конфига

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

const path = require('path');

module.exports = {
  mode: 'development',

  entry: path.resolve(__dirname, 'tests/fixtures/app.js'),

  output: {
    path: path.resolve(__dirname, 'tests/dist'),
    filename: 'bundle.js',
    clean: true
  }
};

Такой конфиг:

  • использует фикстурное приложение;
  • изолирует output;
  • не влияет на production bundle.

Использование webpack Node API

Webpack предоставляет программный API для запуска сборки напрямую из тестов.

Установка зависимостей

npm install webpack jest --save-dev

Базовый интеграционный тест

const webpack = require('webpack');
const config = require('../. ./webpack.test.config');

describe('Webpack build', () => {
  test('should compile successfully', done => {
    webpack(config, (err, stats) => {
      expect(err).toBeNull();

      const info = stats.toJson();

      expect(stats.hasErrors()).toBe(false);
      expect(info.errors.length).toBe(0);

      done();
    });
  });
});

Проверка warnings

Иногда warning считается ошибкой сборки.

expect(stats.hasWarnings()).toBe(false);

Либо анализируется конкретный warning:

const warnings = stats.toJson().warnings;

expect(warnings).not.toContainEqual(
  expect.stringContaining('deprecated')
);

Проверка generated assets

Webpack позволяет получить список файлов сборки.

const assets = stats.toJson().assets;

expect(
  assets.some(asset => asset.name === 'bundle.js')
).toBe(true);

Проверка chunk-структуры

Интеграционные тесты часто проверяют code splitting.

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

optimization: {
  splitChunks: {
    chunks: 'all'
  }
}

Проверка

const chunks = stats.toJson().chunks;

expect(chunks.length).toBeGreaterThan(1);

Тестирование динамических импортов

Исходный код

button.addEventListener('click', async () => {
  const module = await import('./dialog');

  module.openDialog();
});

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

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

  • создание отдельного чанка;
  • отсутствие runtime-ошибок;
  • успешная загрузка модуля.
const assets = stats.toJson().assets;

const hasAsyncChunk = assets.some(asset =>
  asset.name.includes('dialog')
);

expect(hasAsyncChunk).toBe(true);

Проверка tree shaking

Tree shaking — одна из наиболее критичных оптимизаций Webpack.

Исходный модуль

export function used() {
  return 1;
}

export function unused() {
  console.log('remove me');
}

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

import { used } from './math';

used();

Production-конфигурация

mode: 'production',
optimization: {
  usedExports: true
}

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

const fs = require('fs');

const bundle = fs.readFileSync(
  './tests/dist/bundle.js',
  'utf8'
);

expect(bundle.includes('remove me')).toBe(false);

Проверка sideEffects

Webpack учитывает поле sideEffects.

package.json

{
  "sideEffects": false
}

Интеграционные тесты позволяют убедиться, что:

  • неиспользуемые модули удаляются;
  • побочные эффекты не ломают приложение;
  • CSS-файлы не удаляются ошибочно.

Тестирование Babel в Webpack

Webpack и Babel тесно интегрированы через babel-loader.

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

module: {
  rules: [
    {
      test: /\.js$/,
      exclude: /node_modules/,
      use: 'babel-loader'
    }
  ]
}

Проверка транспиляции

ES2022-код

class User {
  #name = 'Alex';

  getName() {
    return this.#name;
  }
}

Проверка результата

const bundle = fs.readFileSync(
  './tests/dist/bundle.js',
  'utf8'
);

expect(bundle.includes('#name')).toBe(false);

Тестирование TypeScript-сборки

Конфигурация ts-loader

{
  test: /\.ts$/,
  use: 'ts-loader'
}

Проверка отсутствия TS-ошибок

expect(stats.compilation.errors.length).toBe(0);

Проверка source maps

Source maps критичны для:

  • debugging;
  • error tracking;
  • stack traces;
  • Sentry-интеграции.

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

devtool: 'source-map'

Проверка

const assets = stats.toJson().assets;

expect(
  assets.some(asset => asset.name.endsWith('.map'))
).toBe(true);

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

Иногда bundle сравнивается через snapshots.

Пример

expect(bundle).toMatchSnapshot();

Однако snapshot bundle имеет недостатки:

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

Поэтому snapshot-тестирование применяется ограниченно.


Проверка размеров bundle

Размер сборки часто контролируется интеграционными тестами.

Пример

const statsData = stats.toJson({
  assets: true
});

const bundleAsset = statsData.assets.find(
  asset => asset.name === 'bundle.js'
);

expect(bundleAsset.size).toBeLessThan(300000);

Анализ duplicate dependencies

Webpack может включать дублирующиеся зависимости.

Интеграционные тесты помогают обнаружить:

  • multiple React copies;
  • дублирование lodash;
  • проблемы module federation;
  • version mismatch.

Тестирование CSS-сборки

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

{
  test: /\.css$/,
  use: [
    'style-loader',
    'css-loader'
  ]
}

Проверка CSS inclusion

const bundle = fs.readFileSync(
  './tests/dist/bundle.js',
  'utf8'
);

expect(bundle.includes('background')).toBe(true);

Тестирование CSS Modules

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

{
  test: /\.module.css$/,
  use: [
    'style-loader',
    {
      loader: 'css-loader',
      options: {
        modules: true
      }
    }
  ]
}

Проверка генерации class names

expect(bundle.includes('_button_')).toBe(true);

Тестирование asset modules

Webpack 5 заменил:

  • file-loader;
  • url-loader;
  • raw-loader.

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

{
  test: /\.(png|jpg)$/i,
  type: 'asset/resource'
}

Проверка asset generation

const assets = stats.toJson().assets;

const imageAsset = assets.find(asset =>
  asset.name.endsWith('.png')
);

expect(imageAsset).toBeDefined();

Тестирование inline assets

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

{
  test: /\.svg$/,
  type: 'asset/inline'
}

Проверка

expect(bundle.includes('data:image/svg+xml')).toBe(true);

Проверка alias-конфигурации

Resolve aliases

resolve: {
  alias: {
    '@': path.resolve(__dirname, 'src')
  }
}

Тест

expect(stats.hasErrors()).toBe(false);

Ошибки alias обычно проявляются как:

Module not found

Тестирование externals

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

externals: {
  react: 'React'
}

Проверка

expect(bundle.includes('React')).toBe(true);
expect(bundle.includes('node_modules/react')).toBe(false);

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

Тестирование webpack-dev-server значительно сложнее обычной сборки.

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

  • HMR;
  • live reload;
  • websocket-подключения;
  • middleware;
  • proxy;
  • overlay ошибок.

Запуск devServer внутри тестов

Пример

const WebpackDevServer = require('webpack-dev-server');
const webpack = require('webpack');

const compiler = webpack(config);

const server = new WebpackDevServer(
  {
    port: 9000
  },
  compiler
);

Проверка HMR

Интеграционные тесты HMR обычно используют:

  • Playwright;
  • Puppeteer;
  • Selenium.

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

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

Интеграция с Playwright

Установка

npm install playwright --save-dev

Полноценный E2E-интеграционный тест

const { test, expect } = require('@playwright/test');

test('application should work after webpack build', async ({
  page
}) => {
  await page.goto('http://localhost:9000');

  await expect(page.locator('h1'))
    .toHaveText('Application');
});

Тестирование production-сборки

Production-сборка имеет особенности:

  • минификация;
  • tree shaking;
  • chunk hashing;
  • runtime extraction;
  • aggressive optimization.

Интеграционные тесты должны запускаться отдельно для production mode.

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

mode: 'production'

Проверка content hashing

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

output: {
  filename: '[name].[contenthash].js'
}

Проверка

const assets = stats.toJson().assets;

expect(
  assets.some(asset =>
    /\.[a-f0-9]{20}\.js$/.test(asset.name)
  )
).toBe(true);

Проверка deterministic ids

Webpack 5 использует deterministic ids.

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

optimization: {
  moduleIds: 'deterministic'
}

Интеграционные тесты помогают контролировать стабильность кэша.


Тестирование Module Federation

Module Federation требует отдельного уровня интеграционных тестов.

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

  • remoteEntry.js;
  • shared dependencies;
  • singleton modules;
  • remote imports;
  • fallback-механизмы.

Проверка remote container

expect(
  assets.some(asset =>
    asset.name === 'remoteEntry.js'
  )
).toBe(true);

Тестирование SSR-сборок

SSR-конфигурации Webpack отличаются:

  • target: node;
  • отсутствие DOM;
  • externalization node_modules;
  • отдельный runtime.

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

target: 'node'

Проверка server bundle

const serverBundle = require(
  '../. ./tests/dist/server.js'
);

expect(serverBundle.render).toBeDefined();

Интеграционное тестирование plugins

Plugins изменяют внутренний pipeline Webpack.

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

  • lifecycle hooks;
  • asset transformations;
  • compilation mutations;
  • emit phase;
  • chunk graph.

Проверка пользовательского plugin

Plugin

class BannerPlugin {
  apply(compiler) {
    compiler.hooks.emit.tap(
      'BannerPlugin',
      compilation => {
        Object.keys(compilation.assets).forEach(
          filename => {
            const source =
              compilation.assets[filename].source();

            compilation.assets[filename] = {
              source: () => '/* banner */\n' + source,
              size: () => source.length
            };
          }
        );
      }
    );
  }
}

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

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

Тестирование watch mode

Watch mode важен для DX и CI-пайплайнов.

Пример

const watching = compiler.watch({}, () => {
  console.log('rebuild');
});

Проверка rebuild

Тесты обычно:

  1. изменяют fixture-файл;
  2. ожидают rebuild;
  3. проверяют обновлённый output.

Очистка временных файлов

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

Очистка после тестов

const fs = require('fs');

afterAll(() => {
  fs.rmSync('./tests/dist', {
    recursive: true,
    force: true
  });
});

Использование memfs

Для ускорения тестов применяется in-memory filesystem.

Установка

npm install memfs --save-dev

Пример использования

const { Volume } = require('memfs');

const vol = new Volume();

compiler.outputFileSystem = vol;

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

  • высокая скорость;
  • отсутствие дисковых операций;
  • изоляция тестов;
  • упрощённый cleanup.

Использование unionfs

unionfs позволяет комбинировать:

  • реальную файловую систему;
  • виртуальную файловую систему.

Это полезно при:

  • тестировании loaders;
  • тестировании plugins;
  • работе с fixture-проектами.

Параллельное выполнение сборок

Интеграционные тесты Webpack часто запускаются параллельно.

Проблемы:

  • конфликты output path;
  • race conditions;
  • shared cache;
  • общие temporary directories.

Решение — генерация уникальных output-директорий.

Пример

const os = require('os');
const path = require('path');

const outputPath = path.join(
  os.tmpdir(),
  `webpack-test-${Date.now()}`
);

Тестирование кеширования

Webpack 5 поддерживает persistent cache.

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

cache: {
  type: 'filesystem'
}

Проверка cache reuse

Интеграционные тесты могут сравнивать:

  • время первой сборки;
  • время повторной сборки;
  • количество rebuilt modules.

Анализ compilation stats

Webpack предоставляет подробную статистику.

Пример

const statsData = stats.toJson({
  all: false,
  assets: true,
  chunks: true,
  modules: true
});

Через stats можно анализировать:

  • размер модулей;
  • dependency graph;
  • chunk composition;
  • orphan modules;
  • optimization bailouts.

Тестирование optimization bailouts

Webpack может отключать оптимизации.

Проверка

const modules = stats.toJson({
  optimizationBailout: true
}).modules;

Такие тесты помогают находить:

  • CommonJS-препятствия;
  • side effects;
  • динамические require;
  • проблемы tree shaking.

Интеграция с CI/CD

Webpack-интеграционные тесты часто выполняются:

  • перед релизом;
  • при merge request;
  • в nightly build;
  • в performance pipeline.

Особенно важно тестировать:

  • production build;
  • legacy browsers;
  • SSR;
  • code splitting;
  • bundle size regression.

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

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

Ошибка:

expect(stats.hasErrors()).toBe(false);

Недостаток — runtime может быть сломан.


Использование production bundle snapshots

Минификация делает snapshots нестабильными.


Общие output directories

Параллельные тесты начинают конфликтовать.


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

Многие warning Webpack сигнализируют о серьёзных проблемах:

  • duplicate packages;
  • large assets;
  • optimization bailout;
  • conflicting chunk names.

Отсутствие runtime-проверок

Даже корректно собранный bundle может падать в браузере.


Практика построения надёжных интеграционных тестов

Наиболее стабильный подход включает:

  1. Изолированный fixture-проект.
  2. Отдельный webpack-config.
  3. Сборку через Node API.
  4. Проверку stats.
  5. Анализ generated assets.
  6. Runtime-тестирование через браузер.
  7. Проверку production mode.
  8. Контроль размеров bundle.
  9. Тестирование code splitting.
  10. Очистку окружения после тестов.

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