Совместимость с Jest

При разработке проектов на базе Vite часто возникает необходимость использовать существующую инфраструктуру тестирования, построенную вокруг Jest. Особенно это актуально при:

  • миграции крупных проектов;
  • переносе конфигураций из Webpack;
  • использовании старых тестовых библиотек;
  • интеграции корпоративных шаблонов;
  • поддержке legacy-кода;
  • работе с React-проектами, созданными до появления Vite.

Несмотря на активное развитие Vitest, Jest остаётся одной из самых распространённых тестовых платформ в экосистеме JavaScript. Поэтому Vite предоставляет различные механизмы совместимости и интеграции.


Архитектурные различия между Vite и Jest

Подход Vite

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

  • ES Modules;
  • нативную загрузку модулей браузером;
  • трансформацию через Rollup и esbuild;
  • dev server вместо традиционного bundling на этапе разработки.

Подход Jest

Jest исторически ориентирован на:

  • CommonJS;
  • собственную систему модулей;
  • runtime-трансформацию файлов;
  • sandbox-окружение;
  • виртуальный module loader.

Из-за этого между Vite и Jest возникают следующие несовместимости:

Область Vite Jest
Модули ESM CJS/частично ESM
Импорт CSS Поддерживается Требует mock
import.meta.env Есть Нет
Алиасы Vite Есть Нужно дублировать
HMR API Есть Нет
URL imports Есть Частично
Asset imports Есть Требует настройки

Использование Jest внутри проекта Vite

Установка Jest

npm install -D jest

Для TypeScript:

npm install -D typescript ts-jest @types/jest

Для Babel:

npm install -D babel-jest @babel/core @babel/preset-env

Базовая структура проекта

project/
├── src/
├── tests/
├── vite.config.js
├── jest.config.js
└── package.json

Настройка Jest для работы с Vite

Минимальная конфигурация

// jest.config.js

export default {
    testEnvironment: 'jsdom'
};

Однако этого недостаточно для полноценной совместимости.


Поддержка ES Modules

Проблема ESM

Vite использует ESM по умолчанию:

import { sum } from './math.js';

Jest долгое время работал исключительно через CommonJS:

const { sum } = require('./math');

Современные версии Jest поддерживают ESM, но требуют дополнительной настройки.


Активация ESM в Jest

package.json

{
  "type": "module"
}

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

// jest.config.js

export default {
    testEnvironment: 'jsdom',
    extensionsToTreatAsEsm: ['.js']
};

Использование Node experimental VM modules

Иногда требуется запуск:

node --experimental-vm-modules node_modules/jest/bin/jest.js

В package.json:

{
  "scripts": {
    "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js"
  }
}

Совместимость import.meta.env

Особенность Vite

Vite внедряет переменные окружения через:

import.meta.env

Пример:

const apiUrl = import.meta.env.VITE_API_URL;

Jest не понимает import.meta.


Ошибка без настройки

SyntaxError: Cannot use 'import.meta' outside a module

Решение через mock

Создание setup-файла

// tests/setup-env.js

global.import = {
    meta: {
        env: {
            VITE_API_URL: 'http://localhost:3000'
        }
    }
};

Подключение setup-файла

// jest.config.js

export default {
    setupFiles: ['./tests/setup-env.js']
};

Альтернативный подход

Лучше изолировать доступ к окружению:

// env.js

export const API_URL = import.meta.env.VITE_API_URL;

Тогда в тестах можно мокать модуль:

jest.mock('./env.js', () => ({
    API_URL: 'mock-url'
}));

Поддержка алиасов Vite

Алиасы в Vite

// vite.config.js

import { defineConfig } from 'vite';
import path from 'path';

export default defineConfig({
    resolve: {
        alias: {
            '@': path.resolve(__dirname, './src')
        }
    }
});

Проблема Jest

Jest не знает о конфигурации Vite.


Настройка moduleNameMapper

// jest.config.js

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

Поддержка нескольких алиасов

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

Работа с CSS и стилями

Импорт CSS в Vite

Vite позволяет:

import './style.css';

Jest не умеет обрабатывать CSS без mock-механизма.


Ошибка

Unexpected token '.'

Решение через identity-obj-proxy

Установка

npm install -D identity-obj-proxy

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

// jest.config.js

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

Для CSS Modules

Компонент

import styles from './Button.module.css';

export function Button() {
    return <button className={styles.btn}>OK</button>;
}

Тест

expect(styles.btn).toBe('btn');

Обработка статических ресурсов

Импорт изображений в Vite

import logo from './logo.png';

Jest не умеет импортировать asset-файлы напрямую.


Настройка mock-файлов

fileMock.js

export default 'test-file-stub';

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

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

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

Зачем нужен Babel

Babel помогает Jest:

  • обрабатывать ESM;
  • поддерживать JSX;
  • компилировать современный JavaScript;
  • преобразовывать нестандартный синтаксис.

Установка

npm install -D babel-jest @babel/core @babel/preset-env

Для React:

npm install -D @babel/preset-react

babel.config.js

export default {
    presets: [
        '@babel/preset-env',
        '@babel/preset-react'
    ]
};

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

export default {
    transform: {
        '^.+\\.jsx?$': 'babel-jest'
    }
};

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

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

npm install -D ts-jest @types/jest

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

// jest.config.js

export default {
    preset: 'ts-jest',
    testEnvironment: 'jsdom'
};

Поддержка TSX

transform: {
    '^.+\\.tsx?$': 'ts-jest'
}

Совместимость React-проектов

Установка React Testing Library

npm install -D @testing-library/react

Настройка jsdom

export default {
    testEnvironment: 'jsdom'
};

Пример теста

import { render, screen } from '@testing-library/react';
import { Button } from './Button';

test('renders button', () => {
    render(<Button />);

    expect(screen.getByText('OK')).toBeInTheDocument();
});

Настройка setupFilesAfterEnv

Расширение matchers

Установка

npm install -D @testing-library/jest-dom

setupTests.js

import '@testing-library/jest-dom';

Подключение

export default {
    setupFilesAfterEnv: ['./tests/setupTests.js']
};

Совместимость с Vue

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

npm install -D @vue/vue3-jest

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

transform: {
    '^.+\\.vue$': '@vue/vue3-jest'
}

Поддержка script setup

Современные версии vue-jest поддерживают:

<script setup>
</script>

Однако возможны проблемы совместимости версий Vue, Jest и Babel.


Работа с async-кодом

Особенности Vite

Vite активно использует:

  • dynamic import;
  • top-level await;
  • lazy loading.

Поддержка dynamic import

Babel plugin

npm install -D babel-plugin-dynamic-import-node

babel.config.js

export default {
    plugins: [
        'dynamic-import-node'
    ]
};

Поддержка import.meta.glob

Возможности Vite

const modules = import.meta.glob('./modules/*.js');

Jest не поддерживает этот API.


Способы обхода

Изоляция функциональности

// loader.js

export function loadModules() {
    return import.meta.glob('./modules/*.js');
}

Mock в тестах

jest.mock('./loader.js', () => ({
    loadModules: () => ({
        './a.js': jest.fn(),
        './b.js': jest.fn()
    })
}));

Поддержка Web Workers

Worker API в Vite

const worker = new Worker(
    new URL('./worker.js', import.meta.url)
);

Jest не поддерживает Worker API из коробки.


Mock Worker

class WorkerMock {
    postMessage() {}
    terminate() {}
}

global.Worker = WorkerMock;

Совместимость с SVG как React-компонентами

Возможность Vite

import Logo from './logo.svg?react';

Jest не понимает query-параметры.


Решение

moduleNameMapper

moduleNameMapper: {
    '\\.svg\\?react$': '<rootDir>/tests/mocks/svgComponentMock.js'
}

svgComponentMock.js

export default function SvgMock() {
    return null;
}

Проблемы transformIgnorePatterns

Причина

Некоторые npm-пакеты публикуются только как ESM.

Jest по умолчанию не трансформирует node_modules.


Ошибка

Unexpected token export

Решение

export default {
    transformIgnorePatterns: [
        '/node_modules/(?!(package-name)/)'
    ]
};

Полная конфигурация Jest для Vite

// jest.config.js

export default {
    testEnvironment: 'jsdom',

    setupFilesAfterEnv: [
        '<rootDir>/tests/setupTests.js'
    ],

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

        '\\.(css|scss|sass)$':
            'identity-obj-proxy',

        '\\.(png|jpg|jpeg|gif|svg)$':
            '<rootDir>/tests/mocks/fileMock.js'
    },

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

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

Использование Jest вместе с Vitest

Гибридный подход

Возможна совместная работа:

  • Vitest для unit-тестов;
  • Jest для legacy-тестов;
  • Jest для специфических плагинов;
  • постепенная миграция.

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

{
  "scripts": {
    "test": "vitest",
    "test:jest": "jest"
  }
}

Миграция с Jest на Vitest

Причины миграции

Vitest:

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

Совместимость API

Vitest специально повторяет API Jest:

describe()
test()
expect()
beforeEach()
afterEach()
vi.fn()

Замена jest на vi

Jest

jest.fn();

Vitest

vi.fn();

Частичная совместимость

Многие тесты можно перенести почти без изменений:

import { describe, test, expect } from 'vitest';

Ограничения совместимости

Полной прозрачности не существует

Несмотря на большое количество настроек, Jest и Vite остаются разными системами.

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

  • import.meta.hot;
  • import.meta.glob;
  • HMR API;
  • worker-модули;
  • URL imports;
  • CSS processing;
  • native ESM edge-cases;
  • plugin hooks Vite.

Рост сложности конфигурации

При увеличении проекта конфигурация Jest может стать значительно сложнее:

  • дублирование alias;
  • отдельные mock-файлы;
  • Babel-конфигурация;
  • ESM-совместимость;
  • polyfills;
  • environment mocks.

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

Выделение платформозависимого кода

Хорошей практикой считается изоляция:

  • import.meta.env;
  • import.meta.glob;
  • Worker API;
  • browser-only API.

Пример адаптера

// platform/env.js

export function getApiUrl() {
    return import.meta.env.VITE_API_URL;
}

Тестирование через mock

jest.mock('./platform/env.js', () => ({
    getApiUrl: () => 'mock-api'
}));

Когда Jest действительно нужен

Сценарии оправданного использования

Jest остаётся актуальным при:

  • больших legacy-проектах;
  • наличии сложной экосистемы плагинов;
  • использовании старых React-шаблонов;
  • корпоративных стандартах;
  • интеграции с существующим CI;
  • монорепозиториях со смешанной архитектурой.

Когда лучше перейти на Vitest

Сценарии полной миграции

Vitest обычно предпочтительнее при:

  • новых Vite-проектах;
  • активном использовании ESM;
  • React 18+;
  • Vue 3;
  • TypeScript-first архитектуре;
  • необходимости высокой скорости тестирования;
  • использовании import.meta API;
  • разработке современных frontend-приложений.