Руководство по настройке Jest и React Testing Library для React и Next.js проектов: от установки
до первых тестов

ноутбук на темно-фиолетовом фоне с двумя логотипами
В данной статье мы подробно рассмотрим процесс настройки среды unit-тестирования веб-приложений на базе React и Next.js с использованием Jest и React Testing Library. Расскажем об установке необходимых зависимостей, создании конфигурационных файлов, настройке Babel и TypeScript, подключении SCSS и SVG,
а также организации структуры проекта. Особое внимание уделено специфике настройки Jest в среде Next.js. Материал будет полезен для frontend-разработчиков
и команд разработки, которые работают с React или Next.js проектами и хотят внедрить unit-тестирование.

Читать на Habr

Jest на проектах React и Next (настройка тестовой среды)

Unit-тесты — это основа тестирования в современной веб-разработке. Они позволяют проверять корректность работы отдельных функций, модулей или компонентов
в изоляции от остальной части приложения. Одним из самых распространённых инструментов для написания таких тестов в JavaScript является Jest — он быстро запускается, поддерживает снимки (snapshot-тесты) и легко интегрируется
с большинством современных фреймворков.

Установка Jest

Для начала работы с Jest необходимо установить его в ваш проект:
npm install --save-dev jest @types/jest
После установки Jest будет добавлен в ваш проект как dev-зависимость
для разработки.

Настройка Jest

Jest не требует сложной настройки и работает «из коробки». Мы рассмотрим вариант, который позволит настроить Jest под конкретный проект.

Создание конфигурации Jest:
npx jest --init
При создании конфигурационного файла будут заданы вопросы:

  • Хотим ли мы установить скрипт test в package.json — yes
  • Используем ли мы TypeScript — yes
  • В качестве тестовой среды выберем — jsdom
  • Хотим ли мы получать отчёт по покрытию тестами — no
  • Выбираем транслятор кода — babel
  • Хотим ли мы очищать моки после каждого теста — yes
На этом установка завершена, мы получили файл jest.config.ts, в котором практически всё закомментировано. Пройдемся по файлу и раскомментируем нужные параметры.
Конфигурация, которая получилась:
export default {
   clearMocks: true,
   testEnvironment: 'jsdom',
   coveragePathIgnorePatterns: ['\\\\node_modules\\\\'],
   moduleFileExtensions: ['js', 'jsx', 'ts', 'tsx', 'json', 'node'],
   moduleDirectories: ['node_modules'],
   modulePaths: ['<rootDir>src'],
   testMatch: [
       '<rootDir>src/**/*(*.)@(spec|test).[tj]s?(x)',
   ],
   rootDir: '../../',
   transformIgnorePatterns: ['node_modules/(?!axios)'],
   reporters: [
       'default',
       [
           'jest-html-reporters',
           {
               publicPath: '<rootDir>/reports/unit',
               filename: 'report.html',
               inlineSource: true,
           },
       ],
   ],
}
Расскажем о некоторых настройках:

  • Для настройки jest мы будем создавать несколько файлов, поэтому вынесем конфигурационный файл из корня проекта в отдельную папку. В rootDir указываем путь до корневой папки проекта, затем добавляем путь до конфигурационного файла в скрипт запуска тестов в package.json в нашем случае это:
test:unit": "jest --config ./config/jest/jest.config.ts
  • testMatch — регулярное выражение, по которому определяются файлы с тестами. У стандартных шаблонов есть различия между macOS и Windows, поэтому используем универсальное выражение с явным указанием корневой папки.
  • modulePaths — Будем использовать абсолютные импорты в тестах.
Прежде чем начать писать unit-тесты, добавим соответствующую настройку в корневой файл .eslintrc.js.
env: {
   jest: true,
},
Если вы решили оставить закомментированными правила в конфигурационном файле, добавьте правило для предотвращения ошибок, вызванных длиной строки:
'max-len': ['error', { ignoreComments: true, code: 125 }],
Создадим тестовый файл testing.test.ts и напишем простой тест для проверки настроек.
describe('tests', () => {
   test('successful test', () => {
       expect(true).toBe(true);
   });
});
Если вы настраиваете проект на React, то на данном этапе уже можно запустить
и получить успешно пройденный тест. А для проекта на Next.js устанавливаем дополнительные пакеты:
npm install -D jest jest-environment-jsdom @testing-library/jest-dom ts-node
Внесем коррективы в jest.config.ts согласно документации.
import { Config } from 'jest';
import nextJest from 'next/jest';


const createJestConfig = nextJest({
   dir: './',
});


const config: Config = {
   clearMocks: true,
   testEnvironment: 'jsdom',
   coverageProvider: 'v8',
   coveragePathIgnorePatterns: ['\\\\node_modules\\\\'],
   moduleFileExtensions: ['js', 'jsx', 'ts', 'tsx', 'json', 'node'],
   moduleDirectories: ['node_modules'],
   modulePaths: ['<rootDir>src'],
   testMatch: ['<rootDir>src/**/*(*.)@(spec|test).[tj]s?(x)'],
   rootDir: '../../',
   transformIgnorePatterns: ['node_modules/(?!axios)'],
   reporters: [
       'default',
       [
           'jest-html-reporters',
           {
               publicPath: '<rootDir>/reports/unit',
               filename: 'report.html',
               inlineSource: true,
           },
       ],
   ],
};


export default createJestConfig(config);
В тестовом файле testing.test.ts возникает ошибка на describe, потому что TypeScript рассматривает файл как глобальный скрипт, а не как модуль.
Чтобы исправить эту ошибку временно, достаточно добавить любой импорт
или экспорт в файл.
export {};
Теперь можем запустить тесты и на Next.js
npm run test:unit. 
И получаем отчет об успешно пройденном тесте.
блок кода
Добавим провальный тест:
describe('testing', () => {
   test('successful test', () => {
       expect(true).toBe(true);
   });
   test('failed test', () => {
       expect(false).toBe(true);
   });
});
Выполняем команду
npm run test:unit
и получаем отчет, в котором указано, что один тест прошел успешно, а второй провалился.
блок кода с тестом
Создадим тестовую функцию:
export const jestTest = (arg: string): string => {
   return arg;
}
Используем ее для тестирования
describe('tests', () => {
   test('successful test', () => {
       expect(true).toBe(true);
   });
   test('failed test', () => {
       expect(false).toBe(true);
   });
   test('jestTest', () => {
       expect(jestTest('jest')).toBe('jest');
   });
});
В проекте на React среда разработки подскажет, что Jest не знает о наличии TypeScript в проекте. Для этого устанавливаем пакет:
npm i @babel/preset-typescript
Добавляем пресет в файле babel.config.json
"presets": [
   "@babel/preset-env",
   "@babel/preset-typescript",
],
Запускаем тесты
npm run test:unit
И всё работает отлично — функция импортировалась, два теста прошли успешно, один провалился.

Тестирование компонентов интерфейса React Testing Library(RTL)

Установка:
 npm install --save-dev @testing-library/react @testing-library/dom
Рядом с файлом конфигурации Jest создаём файл setupTests.ts и добавляем в него:
import '@testing-library/jest-dom';
Добавляем в jest.config.ts путь до файла
 setupFilesAfterEnv: ['<rootDir>config/jest/setupTests.ts']
Свяжем setupTests.ts с tsconfig.json
добавим с указанием пути до файла.
"include": [
   "./config/jest/setupTests.ts",
]
Теперь мы можем использовать все функции для проверки, связанные
с использованием DOM-дерева — такие как проверка стилей, класса, контента или, например, находится ли элемент в DOM-дереве — toBeInTheDocument().
describe('Button', () => {
   test('Проверка отрисовки', () => {
       render(<Button>Test</Button>);
       expect(screen.getByText('Test')).toBeInTheDocument();
   });
});
Функция render из библиотеки RTL (React Testing Library) позволяет нам отрисовать отдельный элемент интерфейса, а также предоставляет объект screen с набором методов, с помощью которых можно получать данные об этом элементе.

На проекте Next.js можно запускать тесты.

Для React-проекта перед запуском тестов установим дополнительный пакет:
npm i @babel/preset-react
И добавим пресет в массив файла babel.config.json
{
   "presets": [
       [
           "@babel/preset-react",
           {
               "runtime": "automatic"
           }
       ]
   ],
}
Установим пакет для настройки работы с scss:
npm i identity-obj-proxy 
Добавляем в файл jest.config.ts настройку для обработки scss:
   moduleNameMapper: {
       '\\.(s?css)$': 'identity-obj-proxy',
   },
Ниже представлена команда, которая запускает тесты для указанного файла.
Для запуска всех unit-тестов используйте команду npm run test:unit.
npm run test:unit Button.test.tsx
Чтобы работать с компонентами, содержащими SVG-файлы в React, добавим соответствующую настройку в конфигурацию Jest.
moduleNameMapper: {
   '\\.(s?css)$': 'identity-obj-proxy',
   '\\.svg': path.resolve(__dirname, 'jestEmptyComponent.tsx'),
},
Здесь мы описываем, что в случае использования расширения .svg будет подключен файл, расположенный по указанному пути. Cоздаём компонент который будет использоваться вместо svg файла.
const jestEmptyComponent = function () {
   return <div />;
};

export default jestEmptyComponent;
В случае с Next.js мы можем создавать моки для каждого используемого в компоненте svg.
jest.mock('../../assets/icons/arrowLeft.svg', () => () => <div data-testid="svg-mock" />);
Но каждый раз создавать моки для каждой иконки не очень удобно, чтобы этого избежать переделаем файл jest.config.ts:
import nextJest from 'next/jest';
import path from 'path';


const customJestConfig = {
   setupFilesAfterEnv: ['<rootDir>config/jest/setupTests.ts'],
   clearMocks: true,
   testEnvironment: 'jsdom',
   coverageProvider: 'v8',
   coveragePathIgnorePatterns: ['\\\\node_modules\\\\'],
   moduleFileExtensions: ['js', 'jsx', 'ts', 'tsx', 'json', 'node'],
   moduleDirectories: ['node_modules'],
   modulePaths: ['<rootDir>src'],
   testMatch: ['<rootDir>src/**/*(*.)@(spec|test).[tj]s?(x)'],
   rootDir: '../../',
   transformIgnorePatterns: ['node_modules/(?!axios)'],
   reporters: [
       'default',
       [
           'jest-html-reporters',
           {
               publicPath: '<rootDir>/reports/unit',
               filename: 'report.html',
               inlineSource: true,
           },
       ],
   ],
};


const createJestConfig = nextJest({
   dir: './',
});


const jestConfig = async () => {
   const nextJestConfig = await createJestConfig(customJestConfig)();
   return {
       ...nextJestConfig,
       moduleNameMapper: {
           '\\.svg$': path.resolve(__dirname, 'jestEmptyComponent.tsx'),
           ...nextJestConfig.moduleNameMapper,
       },
   };
};


export default jestConfig;
Все настройки Jest мы определили в объекте customJestConfig, а функцию createJestConfig оставили без изменений, чтобы учитывать стандарты Next.js.

Создание конфигурации jestConfig:

nextJestConfig создаёт базовую конфигурацию Jest для проекта на Next.js, учитывая все необходимые оптимизации и специфику работы с использованием функции next/jest. В параметрах передаём наш объект конфигурации.

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

Запускаем тест:
describe('Button Component', () => {
   it('Должен отображаться с левым значком', () => {
       render(<Button addonLeft={<ArrowTestIcon />}>Click Me</Button>);
       expect(screen.getByText('Click Me')).toBeInTheDocument();
   });
});
Тест проходит успешно

Чтобы запускать тесты для асинхронных операций на React нам не требуется менять конфиг подобным образом мы можем просто установить пакет:
npm i -D regenerator-runtime
и импортировать его в setypTests.ts
import 'regenerator-runtime/runtime';
Теперь мы можем удалить наши временные файлы для тестирования настроек — среда разработки готова к unit тестированию.

Заключение

Jest — это мощный и гибкий инструмент для написания unit-тестов в JavaScript.
Он прост в настройке, с богатым набором функций и отличной интеграцией
с современными фреймворками, такими как React и Next.js. Использование Jest
не только улучшает качество кода и уменьшает количество ошибок, но и ускоряет процесс разработки, позволяя сосредоточиться на функциональности, а не на отладке. Начать писать unit-тесты с Jest можно быстро, и уже после нескольких тестов вы заметите, как это упрощает поддержку и развитие вашего приложения.