При тестировании JavaScript-приложений через Jest и Webpack часто возникает проблема обработки статических файлов:
Webpack умеет работать с такими ресурсами благодаря loader-механизму, однако среда Jest не использует Webpack во время выполнения тестов. Из-за этого импорт файла может приводить к ошибкам:
SyntaxError: Unexpected token
Cannot find module './styles.scss'
или:
Jest encountered an unexpected token
Мокирование статических ресурсов позволяет:
Webpack преобразует импорты при помощи loader:
import './styles.scss';
import logo from './logo.png';
Во время сборки Webpack:
Jest работает иначе:
.scss, .png,
.svg.Поэтому требуется дополнительная конфигурация.
Существует несколько основных стратегий:
| Подход | Назначение |
|---|---|
moduleNameMapper |
Подмена файлов-заглушек |
identity-obj-proxy |
Мок CSS Modules |
| Ручные mock-файлы | Гибкое мокирование |
| Трансформеры | Обработка ресурсов |
jest.mock() |
Точечное мокирование |
Основной механизм мокирования статических ресурсов в Jest —
moduleNameMapper.
Пример:
module.exports = {
moduleNameMapper: {
'\\.(css|scss)$': '<rootDir>/__mocks__/styleMock.js',
'\\.(png|jpg|svg)$': '<rootDir>/__mocks__/fileMock.js',
},
};
module.exports = {};
Файл:
__mocks__/styleMock.js
Теперь импорт:
import './styles.scss';
не вызовет ошибку.
module.exports = 'test-file-stub';
Конфигурация:
module.exports = {
moduleNameMapper: {
'\\.(jpg|jpeg|png|gif|svg)$':
'<rootDir>/__mocks__/fileMock.js',
},
};
Теперь:
import logo from './logo.png';
вернет:
'test-file-stub'
На практике обычно мокируются все основные типы ресурсов.
module.exports = {
moduleNameMapper: {
'\\.(css|less|scss|sass)$':
'<rootDir>/__mocks__/styleMock.js',
'\\.(gif|ttf|eot|svg|png|jpg|jpeg)$':
'<rootDir>/__mocks__/fileMock.js',
},
};
При использовании CSS Modules:
import styles from './Button.module.scss';
button.className = styles.primary;
обычный мок:
module.exports = {};
сломает тесты:
styles.primary === undefined
Для CSS Modules используется пакет:
Установка:
npm install identity-obj-proxy --save-dev
module.exports = {
moduleNameMapper: {
'\\.module\\.(css|scss)$': 'identity-obj-proxy',
'\\.(css|scss)$':
'<rootDir>/__mocks__/styleMock.js',
},
};
Импорт:
import styles from './Button.module.scss';
Класс:
styles.primary
вернет:
'primary'
Это особенно важно для:
.primary {
color: white;
}
import styles from './Button.module.scss';
export function Button() {
return (
<button className={styles.primary}>
Click
</button>
);
}
import { render, screen } from '@testing-library/react';
import { Button } from './Button';
test('button has class', () => {
render(<Button />);
const button = screen.getByRole('button');
expect(button.className).toBe('primary');
});
SVG может использоваться несколькими способами:
import icon from './icon.svg';
или:
import { ReactComponent as Icon } from './icon.svg';
Webpack через @svgr/webpack умеет превращать SVG в
React-компонент.
Jest — нет.
module.exports = {
__esModule: true,
default: 'SvgMock',
ReactComponent: 'svg',
};
module.exports = {
moduleNameMapper: {
'\\.svg$': '<rootDir>/__mocks__/svgMock.js',
},
};
import Icon, { ReactComponent } from './icon.svg';
console.log(Icon);
console.log(ReactComponent);
Результат:
'SvgMock'
'svg'
Шрифты также импортируются Webpack:
import './fonts/Roboto.woff2';
Jest не умеет их обрабатывать.
module.exports = {
moduleNameMapper: {
'\\.(woff|woff2|eot|ttf|otf)$':
'<rootDir>/__mocks__/fileMock.js',
},
};
Аудио и видео:
import sound from './sound.mp3';
import video from './movie.mp4';
module.exports = {
moduleNameMapper: {
'\\.(mp4|mp3|wav|avi)$':
'<rootDir>/__mocks__/fileMock.js',
},
};
module.exports = {
moduleNameMapper: {
'\\.module\\.(css|scss|sass)$':
'identity-obj-proxy',
'\\.(css|scss|sass|less)$':
'<rootDir>/__mocks__/styleMock.js',
'\\.(jpg|jpeg|png|gif|webp|svg)$':
'<rootDir>/__mocks__/fileMock.js',
'\\.(woff|woff2|eot|ttf|otf)$':
'<rootDir>/__mocks__/fileMock.js',
'\\.(mp4|mp3|wav)$':
'<rootDir>/__mocks__/fileMock.js',
},
};
Порядок правил важен.
Неправильно:
'\\.(css|scss)$': 'styleMock',
'\\.module\\.(css|scss)$': 'identity-obj-proxy',
Первое правило перехватит все файлы.
Правильно:
'\\.module\\.(css|scss)$': 'identity-obj-proxy',
'\\.(css|scss)$': 'styleMock',
Иногда статические ресурсы проходят через transform.
Пример:
module.exports = {
transform: {
'^.+\\.[jt]sx?$': 'babel-jest',
},
};
Статические ресурсы обычно исключаются из transform и мокируются отдельно.
Мок подходит не всегда.
Иногда необходимо:
В этом случае используются:
jest-transform-stub;babel-jest;ts-jest.Пакет:
Установка:
npm install --save-dev jest-transform-stub
module.exports = {
transform: {
'^.+\\.(css|scss|png|jpg|svg)$':
'jest-transform-stub',
},
};
Подменяет импорт:
import './style.scss';
на mock-модуль.
Преобразует содержимое файла перед исполнением.
Подходит для:
Подходит для:
Иногда мок требуется только в одном тесте.
jest.mock('./logo.png', () => 'logo-file');
jest.mock('./icon.svg', () => ({
__esModule: true,
default: 'Icon',
ReactComponent: 'svg',
}));
Jest поддерживает автоматические mocks.
__mocks__/
fileMock.js
styleMock.js
svgMock.js
Иногда полезно возвращать имя файла.
module.exports = 'mock-file';
или:
module.exports = {
src: 'mock-file',
};
TypeScript не знает о статических импортируемых файлах.
Появляются ошибки:
Cannot find module './logo.svg'
declare module '*.scss';
declare module '*.css';
declare module '*.png' {
const value: string;
export default value;
}
declare module '*.svg' {
const value: string;
export default value;
}
declare module '*.svg' {
import React = require('react');
export const ReactComponent:
React.FC<React.SVGProps<SVGSVGElement>>;
const src: string;
export default src;
}
Статические ресурсы часто используются в компонентах.
import logo from './logo.png';
export function Header() {
return <img src={logo} alt="logo" />;
}
import { render, screen } from '@testing-library/react';
import { Header } from './Header';
test('renders logo', () => {
render(<Header />);
const image = screen.getByAltText('logo');
expect(image.src).toContain('test-file-stub');
});
Без мокирования snapshot может содержать:
Моки стабилизируют snapshots.
<img src="/assets/logo.8f23ab1.png">
<img src="test-file-stub">
Webpack позволяет:
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
},
}
Jest не знает об alias.
module.exports = {
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
},
};
module.exports = {
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
'\\.module\\.(scss|css)$':
'identity-obj-proxy',
'\\.(scss|css)$':
'<rootDir>/__mocks__/styleMock.js',
'\\.(png|svg|jpg)$':
'<rootDir>/__mocks__/fileMock.js',
},
};
Ошибка:
'.css$'
Правильно:
'\\.css$'
Ошибка:
'__mocks__/fileMock.js'
Правильно:
'<rootDir>/__mocks__/fileMock.js'
Неправильный порядок:
'\\.(css|scss)$'
'\\.module\\.(css|scss)$'
Специфичные правила всегда размещаются выше общих.
При использовании ESM:
export default 'file';
CommonJS-вариант может вызывать ошибки.
export default 'test-file-stub';
или:
export default {};
Универсальный mock:
module.exports = 'test-file-stub';
module.exports.default = 'test-file-stub';
В monorepo часто используется общий Jest preset.
module.exports = {
moduleNameMapper: {
'\\.(css|scss)$':
'<rootDir>/config/jest/styleMock.js',
},
};
Крупные проекты обычно выносят mocks:
config/
jest/
fileMock.js
styleMock.js
svgMock.js
Это упрощает:
module.exports = {
testEnvironment: 'jsdom',
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
'\\.module\\.(css|scss|sass)$':
'identity-obj-proxy',
'\\.(css|scss|sass|less)$':
'<rootDir>/config/jest/styleMock.js',
'\\.(jpg|jpeg|png|gif|svg|webp)$':
'<rootDir>/config/jest/fileMock.js',
'\\.(woff|woff2|eot|ttf|otf)$':
'<rootDir>/config/jest/fileMock.js',
},
transform: {
'^.+\\.[jt]sx?$': 'babel-jest',
},
};
moduleNameMapper;styleMock;fileMock;identity-obj-proxy.Особенно при использовании:
В большинстве unit-тестов достаточно обычных mock-файлов.
Это сохраняет:
Единая директория:
config/jest/
делает конфигурацию предсказуемой и удобной для масштабирования.