Next.js - это не просто React поверх сервера. Проект собирается компилятором Next, который понимает вещи, о которых обычный запуск Jest не догадывается: псевдонимы путей из tsconfig, CSS-модули, шрифты через next/font, импорт картинок как модулей, переменные окружения из .env. Тест же исполняет не сборка Next, а Jest напрямую - без всего этого контекста, и он спотыкается на первом же import.
Наивный ход - описать всю эту инфраструктуру в jest.config руками: объявить transform, добавить moduleNameMapper, чтобы CSS-импорты превращались в заглушку, замокать next/font, прописать пути. Это работает ровно до первого расхождения. Как только настоящая сборка меняет способ трансформации или начинает обрабатывать новый тип импорта, ваш тестовый конфиг остается в прошлом. Тесты падают не на вашем коде, а на инфраструктуре, которую вы продублировали и забыли синхронизировать.
Правильная граница проведена за вас. Пакет Next отдает фабрику nextJest: вызов nextJest({ dir: './' }) читает next.config проекта и возвращает функцию createJestConfig. Вы передаете ей свой минимальный конфиг, а она дописывает все, что знает про сборку: transform через компилятор Next (тот же SWC, что и в проде), автоматический мок CSS и .module.css вместе с их scss-вариантами, обработку импорта картинок и next/font, загрузку .env в process.env. Получается одна точка правды - реальная конфигурация проекта, а не ее копия.
За вами остается только то, что относится к тесту как к тесту. Поле testEnvironment выбирает мир, в котором исполняется код: jsdom - эмуляция браузера для компонентов, node - для чистой логики без DOM. Поле setupFilesAfterEnv подключает файл, который выполняется перед каждым набором: туда идут расширения expect (например, матчеры Testing Library) и общие настройки. Все остальное createJestConfig подставит сам.
App Router меняет не столько конфиг, сколько сам вопрос о том, что вообще поддается юнит-тесту. Кода теперь три сорта, и каждый проверяется по-своему. Client Component - помеченный директивой 'use client' - это привычный React: он рендерится в jsdom и проверяется через Testing Library глазами пользователя, по ролям и действиям. Чистая серверная функция - расчет суммы корзины, валидация заказа - про React не знает вовсе и тестируется в среде node как обычный модуль.
Третий сорт - асинхронный Server Component - юнит-тесту не поддается, и это не каприз Jest, а природа RSC. Такой компонент представляет собой async-функцию, которая исполняется на сервере: ждет данные, обращается к server-only ресурсам, отдает поток, который каркас собирает в разметку. jsdom же - это эмуляция браузера, а не сервера Next. Он не воспроизводит серверный lifecycle: нет настоящего рендера асинхронного дерева, нет стриминга, нет окружения, в котором server-only модули имеют смысл. Отрендерить такой компонент в jsdom значит проверить фикцию, а не поведение.
Отсюда честное распределение работы. Логику выносите в чистые функции и покрывайте в node - быстро и детерминированно. Client Components проверяйте в jsdom по ролям и действиям. Связку сервер отдал данные - страница показала заказ закрывайте end-to-end: Playwright или Cypress поднимают настоящий Next и проходят сценарий в браузере. Типичный провал - когда async Server Component тащат в jsdom, обвешивают моками server-only API и получают зеленый тест, который не падает даже на пустой странице в проде. Тест есть, гарантии нет.
import nextJest from 'next/jest.js'
import type { Config } from 'jest'
const createJestConfig = nextJest({ dir: './' })
const config: Config = {
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['<rootDir>/test/setup.ts'],
}
export default createJestConfig(config)