babel-jest и ts-jest в обход Webpack

При тестировании приложений на JavaScript и TypeScript часто возникает проблема несовместимости исходного кода с окружением выполнения тестов. Код может содержать:

  • синтаксис ESNext;
  • TypeScript-аннотации;
  • JSX;
  • decorators;
  • import/export;
  • experimental features;
  • алиасы путей;
  • нестандартные расширения модулей.

Webpack обычно решает эти задачи на этапе сборки, однако Jest запускает файлы напрямую внутри Node.js и не использует Webpack автоматически. Из-за этого тестовая среда не понимает современный синтаксис без дополнительной трансформации.

Для решения этой проблемы используются:

  • Babel + babel-jest;
  • TypeScript + ts-jest;
  • комбинированные схемы Babel и TypeScript.

Почему Jest работает в обход Webpack

Webpack выполняет:

  • bundling;
  • transpilation;
  • tree shaking;
  • обработку loaders;
  • оптимизацию;
  • преобразование импортов.

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

При запуске теста Jest:

  1. Находит файл.
  2. Читает исходный код.
  3. Передаёт код трансформеру.
  4. Выполняет результат в Node.js.

Webpack в этой цепочке отсутствует.

Из-за этого код вроде:

import Button from '@/components/Button';

const element = <Button />;

не будет работать без дополнительных преобразований.


Архитектура трансформации в Jest

Jest использует систему transform.

Пример:

module.exports = {
  transform: {
    '^.+\\.jsx?$': 'babel-jest'
  }
};

Алгоритм работы:

  1. Jest находит файл.
  2. Проверяет регулярное выражение.
  3. Передаёт файл трансформеру.
  4. Трансформер возвращает JavaScript.
  5. Jest исполняет результат.

babel-jest

Что делает babel-jest

babel-jest — официальный трансформер Jest для Babel.

Он:

  • подключает Babel;
  • компилирует современный JavaScript;
  • поддерживает JSX;
  • поддерживает experimental syntax;
  • интегрируется с Babel plugins и presets.

Установка babel-jest

npm install --save-dev jest babel-jest @babel/core

Для React:

npm install --save-dev @babel/preset-env @babel/preset-react

Для TypeScript через Babel:

npm install --save-dev @babel/preset-typescript

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

babel.config.js

module.exports = {
  presets: [
    '@babel/preset-env',
    '@babel/preset-react'
  ]
};

Для TypeScript:

module.exports = {
  presets: [
    '@babel/preset-env',
    '@babel/preset-react',
    '@babel/preset-typescript'
  ]
};

Конфигурация Jest с babel-jest

module.exports = {
  transform: {
    '^.+\\.[jt]sx?$': 'babel-jest'
  }
};

Регулярное выражение:

^.+\\.[jt]sx?$

поддерживает:

  • .js
  • .jsx
  • .ts
  • .tsx

Как babel-jest обрабатывает код

Исходный файл:

const App = () => {
  return <div>Hello</div>;
};

После Babel:

const App = () => {
  return React.createElement("div", null, "Hello");
};

Node.js уже способен выполнить такой код.


Babel без проверки типов

Ключевая особенность Babel:

  • Babel удаляет TypeScript-типы;
  • Babel НЕ проверяет корректность типов.

Пример:

const value: string = 100;

Babel преобразует это в:

const value = 100;

Ошибки типов обнаружены не будут.


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

При использовании Babel обычно добавляют отдельную проверку:

tsc --noEmit

Часто это выполняется:

  • в CI;
  • в pre-commit hooks;
  • отдельным npm script.

Пример:

{
  "scripts": {
    "typecheck": "tsc --noEmit"
  }
}

ts-jest

Назначение ts-jest

ts-jest — специализированный трансформер Jest для TypeScript.

Он:

  • использует TypeScript compiler API;
  • понимает tsconfig.json;
  • поддерживает типизацию;
  • умеет работать без Babel.

Установка ts-jest

npm install --save-dev jest ts-jest typescript

Инициализация ts-jest

npx ts-jest config:init

Автоматически создаётся конфигурация:

module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node'
};

Как работает ts-jest

ts-jest использует TypeScript compiler вместо Babel.

Алгоритм:

  1. Jest передаёт файл.
  2. ts-jest вызывает TypeScript compiler.
  3. TypeScript преобразует код.
  4. Результат возвращается Jest.

Пример tsconfig.json

{
  "compilerOptions": {
    "target": "ES2019",
    "module": "commonjs",
    "jsx": "react-jsx",
    "strict": true
  }
}

Проверка типов в ts-jest

В отличие от Babel, ts-jest умеет диагностировать ошибки типов.

Пример:

const value: string = 100;

Во время тестов может появиться ошибка TypeScript.


isolatedModules

Для ускорения компиляции часто используется:

globals: {
  'ts-jest': {
    isolatedModules: true
  }
}

В этом режиме:

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

Сравнение babel-jest и ts-jest

Возможность babel-jest ts-jest
Поддержка Babel plugins Да Частично
Проверка типов Нет Да
Скорость Выше Ниже
JSX Да Да
Experimental syntax Отлично Ограниченно
Совместимость с React ecosystem Отлично Хорошо
Использование tsconfig Частично Полностью
Работа с decorators Через Babel plugins Через TypeScript

Когда использовать babel-jest

babel-jest предпочтителен, если:

  • проект уже использует Babel;
  • нужен React;
  • используются experimental features;
  • важна скорость тестов;
  • типы проверяются отдельно через tsc.

Когда использовать ts-jest

ts-jest подходит, если:

  • проект полностью основан на TypeScript;
  • требуется строгая типизация в тестах;
  • не используется сложная Babel-конфигурация;
  • нужен полный контроль TypeScript compiler.

Совместное использование Babel и ts-jest

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

Схема:

TypeScript
   ↓
ts-jest
   ↓
Babel
   ↓
Jest runtime

Либо:

TypeScript
   ↓
Babel preset-typescript
   ↓
Jest

Babel preset-typescript

@babel/preset-typescript удаляет типы, но:

  • не проверяет типизацию;
  • не поддерживает некоторые TypeScript-фичи;
  • работает быстрее.

Поддержка JSX

babel-jest

presets: [
  '@babel/preset-react'
]

ts-jest

Поддержка зависит от:

{
  "jsx": "react-jsx"
}

Работа с ESM

Современные проекты всё чаще используют:

import value from './module.js';

Node.js и Jest долгое время ориентировались на CommonJS:

const value = require('./module');

Проблемы ESM в Jest

Без настройки возникают ошибки:

Cannot use import statement outside a module

или:

Unexpected token export

Babel и ESM

Babel может преобразовать ESM в CommonJS:

presets: [
  [
    '@babel/preset-env',
    {
      modules: 'commonjs'
    }
  ]
]

ts-jest и ESM

Для ESM требуется:

module.exports = {
  preset: 'ts-jest/presets/default-esm'
};

Также часто используется:

{
  "module": "ESNext"
}

moduleNameMapper вместо resolve.alias

Webpack использует:

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

Jest этого не понимает.

Необходима отдельная настройка:

moduleNameMapper: {
  '^@/(.*)$': '<rootDir>/src/$1'
}

Почему aliases не работают автоматически

Webpack:

  • анализирует import;
  • изменяет пути;
  • подключает loaders.

Jest:

  • не знает о webpack.config.js;
  • использует собственный resolver.

Трансформация node_modules

По умолчанию Jest игнорирует:

node_modules

Причина — производительность.


transformIgnorePatterns

Иногда пакеты публикуются в неподдерживаемом синтаксисе:

Unexpected token export

Тогда используется:

transformIgnorePatterns: [
  '/node_modules/(?!(my-esm-lib)/)'
]

Пример полной конфигурации babel-jest

jest.config.js

module.exports = {
  testEnvironment: 'jsdom',

  transform: {
    '^.+\\.[jt]sx?$': 'babel-jest'
  },

  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1'
  },

  transformIgnorePatterns: [
    '/node_modules/(?!(some-esm-package)/)'
  ]
};

babel.config.js

module.exports = {
  presets: [
    [
      '@babel/preset-env',
      {
        targets: {
          node: 'current'
        }
      }
    ],

    '@babel/preset-react',
    '@babel/preset-typescript'
  ]
};

Пример полной конфигурации ts-jest

jest.config.js

module.exports = {
  preset: 'ts-jest',

  testEnvironment: 'node',

  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1'
  }
};

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

TypeScript поддерживает:

{
  "paths": {
    "@/*": ["src/*"]
  }
}

Jest автоматически это не читает.


pathsToModuleNameMapper

ts-jest предоставляет helper:

const { pathsToModuleNameMapper } = require('ts-jest');
const { compilerOptions } = require('./tsconfig');

module.exports = {
  moduleNameMapper: pathsToModuleNameMapper(
    compilerOptions.paths,
    { prefix: '<rootDir>/' }
  )
};

Source Maps

Для корректных stack traces используются source maps.

Babel:

sourceMaps: 'inline'

TypeScript:

{
  "sourceMap": true
}

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

babel-jest

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

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

Недостаток:

  • отсутствие type checking.

ts-jest

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

  • полноценный TypeScript analysis;
  • использование compiler API.

Недостатки:

  • медленнее;
  • больше потребление памяти;
  • сложнее cache invalidation.

Кеширование трансформаций

Jest кеширует результат компиляции.

Кеш зависит от:

  • содержимого файла;
  • конфигурации Babel;
  • конфигурации Jest;
  • версии трансформеров.

Очистка:

jest --clearCache

Диагностика ошибок трансформации

Unexpected token

Обычно означает:

  • файл не прошёл через transform;
  • Babel preset отсутствует;
  • ESM не преобразован.

Cannot use import statement outside a module

Причины:

  • неправильная конфигурация ESM;
  • отсутствует Babel transform;
  • неверный target module.

JSX syntax extension is not currently enabled

Отсутствует:

@babel/preset-react

SyntaxError: Unexpected token ‘<’

JSX не был преобразован.


Babel plugins в тестах

Можно использовать любые Babel plugins:

plugins: [
  '@babel/plugin-proposal-decorators'
]

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

  • decorators;
  • class properties;
  • pipeline operator;
  • macros;
  • experimental syntax.

Различия среды Webpack и Jest

Webpack поддерживает:

  • file-loader;
  • css-loader;
  • asset modules;
  • SVG imports;
  • raw-loader.

Jest не умеет это выполнять напрямую.


Mock файловых ресурсов

Для CSS:

moduleNameMapper: {
  '\\.(css|scss)$': 'identity-obj-proxy'
}

Для изображений:

moduleNameMapper: {
  '\\.(png|jpg|svg)$': '<rootDir>/__mocks__/fileMock.js'
}

fileMock.js

module.exports = 'test-file-stub';

React Testing Library и Babel

React Testing Library почти всегда используется вместе с Babel.

Причины:

  • JSX;
  • automatic runtime;
  • React transform;
  • современный JavaScript.

ts-jest в monorepo

В monorepo часто возникают проблемы:

  • разные tsconfig;
  • разные rootDir;
  • конфликты paths;
  • дублирование transform.

projects в Jest

Для monorepo используется:

module.exports = {
  projects: [
    '<rootDir>/packages/app',
    '<rootDir>/packages/core'
  ]
};

Babel cache и CI

В CI кеш иногда становится причиной нестабильности.

Отключение:

jest --no-cache

Hybrid-подход

Распространённая современная схема:

  • Babel используется для transpilation;
  • TypeScript используется только для type checking.

Причины популярности:

  • высокая скорость;
  • гибкость Babel ecosystem;
  • лучшая совместимость с React;
  • упрощённая конфигурация.

SWC и альтернатива Babel

Современные проекты иногда заменяют Babel на:

  • SWC;
  • esbuild.

Для Jest существуют:

  • @swc/jest;
  • esbuild-jest.

Они работают быстрее, однако:

  • имеют ограничения;
  • поддерживают не все Babel plugins;
  • иногда несовместимы с нестандартной трансформацией.

Типичная архитектура современного React-проекта

Webpack/Vite → production build

Jest
  ↓
babel-jest
  ↓
Babel
  ↓
Node.js runtime

TypeScript
  ↓
tsc --noEmit

Такая схема разделяет:

  • сборку;
  • тестирование;
  • типизацию;
  • production optimization.

Это уменьшает связанность инструментов и ускоряет инфраструктуру проекта.