إعداد Jest مع Next.js

يتم استخدام Jest ومكتبة اختبار React (React Testing Library) معًا بشكل متكرر لـ اختبار الوحدات (Unit Testing) واختبار اللقطات (Snapshot Testing). سيوضح لك هذا الدليل كيفية إعداد Jest مع Next.js وكتابة أول اختباراتك.

معلومة مفيدة: نظرًا لأن مكونات الخادم غير المتزامنة (async Server Components) جديدة في نظام React البيئي، فإن Jest لا يدعمها حاليًا. بينما يمكنك仍然 تشغيل اختبارات الوحدات لمكونات الخادم والعميل المتزامنة، نوصي باستخدام اختبارات من النهاية إلى النهاية (E2E tests) للمكونات غير المتزامنة (async).

البدء السريع

يمكنك استخدام create-next-app مع مثال Next.js with-jest للبدء بسرعة:

Terminal
npx create-next-app@latest --example with-jest with-jest-app

الإعداد اليدوي

منذ إصدار Next.js 12، أصبح لدى Next.js الآن تكوين مدمج لـ Jest.

لإعداد Jest، قم بتثبيت jest والحزم التالية كتبعيات تطوير:

Terminal
npm install -D jest jest-environment-jsdom @testing-library/react @testing-library/jest-dom
# أو
yarn add -D jest jest-environment-jsdom @testing-library/react @testing-library/jest-dom
# أو
pnpm install -D jest jest-environment-jsdom @testing-library/react @testing-library/jest-dom

قم بإنشاء ملف تكوين أساسي لـ Jest عن طريق تشغيل الأمر التالي:

Terminal
npm init jest@latest
# أو
yarn create jest@latest
# أو
pnpm create jest@latest

سيأخذك هذا عبر سلسلة من التعليمات لإعداد Jest لمشروعك، بما في ذلك إنشاء ملف jest.config.ts|js تلقائيًا.

قم بتحديث ملف التكوين الخاص بك لاستخدام next/jest. يحتوي هذا المحول على جميع خيارات التكوين اللازمة ليعمل Jest مع Next.js:

import type { Config } from 'jest'
import nextJest from 'next/jest.js'

const createJestConfig = nextJest({
  // قم بتوفير المسار إلى تطبيق Next.js الخاص بك لتحميل next.config.js وملفات .env في بيئة الاختبار
  dir: './',
})

// أضف أي تكوين مخصص لتمريره إلى Jest
const config: Config = {
  coverageProvider: 'v8',
  testEnvironment: 'jsdom',
  // أضف المزيد من خيارات الإعداد قبل تشغيل كل اختبار
  // setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
}

// يتم تصدير createJestConfig بهذه الطريقة لضمان أن next/jest يمكنه تحميل تكوين Next.js وهو غير متزامن
export default createJestConfig(config)
const nextJest = require('next/jest')

/** @type {import('jest').Config} */
const createJestConfig = nextJest({
  // قم بتوفير المسار إلى تطبيق Next.js الخاص بك لتحميل next.config.js وملفات .env في بيئة الاختبار
  dir: './',
})

// أضف أي تكوين مخصص لتمريره إلى Jest
const config = {
  coverageProvider: 'v8',
  testEnvironment: 'jsdom',
  // أضف المزيد من خيارات الإعداد قبل تشغيل كل اختبار
  // setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
}

// يتم تصدير createJestConfig بهذه الطريقة لضمان أن next/jest يمكنه تحميل تكوين Next.js وهو غير متزامن
module.exports = createJestConfig(config)

تحت الغطاء، يقوم next/jest بتكوين Jest تلقائيًا لك، بما في ذلك:

  • إعداد transform باستخدام مترجم Next.js
  • المحاكاة التلقائية لملفات الأنماط (.css, .module.css، ومتغيرات scss الخاصة بها)، واستيراد الصور وnext/font
  • تحميل .env (وجميع المتغيرات) إلى process.env
  • تجاهل node_modules من تحويلات الاختبار والحل
  • تجاهل .next من حل الاختبار
  • تحميل next.config.js للأعلام التي تمكّن تحويلات SWC

معلومة مفيدة: لاختبار متغيرات البيئة مباشرة، قم بتحميلها يدويًا في سيناريو إعداد منفصل أو في ملف jest.config.ts. لمزيد من المعلومات، يرجى الاطلاع على متغيرات بيئة الاختبار.

إعداد Jest (مع Babel)

إذا اخترت عدم استخدام مترجم Next.js واستخدمت Babel بدلاً من ذلك، فستحتاج إلى تكوين Jest يدويًا وتثبيت babel-jest وidentity-obj-proxy بالإضافة إلى الحزم المذكورة أعلاه.

فيما يلي الخيارات الموصى بها لتكوين Jest لـ Next.js:

jest.config.js
module.exports = {
  collectCoverage: true,
  // في node 14.x يوفر coverage provider v8 سرعة جيدة وتقرير جيد إلى حد ما
  coverageProvider: 'v8',
  collectCoverageFrom: [
    '**/*.{js,jsx,ts,tsx}',
    '!**/*.d.ts',
    '!**/node_modules/**',
    '!<rootDir>/out/**',
    '!<rootDir>/.next/**',
    '!<rootDir>/*.config.js',
    '!<rootDir>/coverage/**',
  ],
  moduleNameMapper: {
    // التعامل مع استيراد CSS (مع وحدات CSS)
    // https://jestjs.io/docs/webpack#mocking-css-modules
    '^.+\\.module\\.(css|sass|scss)$': 'identity-obj-proxy',

    // التعامل مع استيراد CSS (بدون وحدات CSS)
    '^.+\\.(css|sass|scss)$': '<rootDir>/__mocks__/styleMock.js',

    // التعامل مع استيراد الصور
    // https://jestjs.io/docs/webpack#handling-static-assets
    '^.+\\.(png|jpg|jpeg|gif|webp|avif|ico|bmp|svg)$/i': `<rootDir>/__mocks__/fileMock.js`,

    // التعامل مع أسماء الوحدات المستعارة
    '^@/components/(.*)$': '<rootDir>/components/$1',

    // التعامل مع @next/font
    '@next/font/(.*)': `<rootDir>/__mocks__/nextFontMock.js`,
    // التعامل مع next/font
    'next/font/(.*)': `<rootDir>/__mocks__/nextFontMock.js`,
    // تعطيل server-only
    'server-only': `<rootDir>/__mocks__/empty.js`,
  },
  // أضف المزيد من خيارات الإعداد قبل تشغيل كل اختبار
  // setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
  testPathIgnorePatterns: ['<rootDir>/node_modules/', '<rootDir>/.next/'],
  testEnvironment: 'jsdom',
  transform: {
    // استخدم babel-jest لتحويل الاختبارات مع الإعداد المسبق next/babel
    // https://jestjs.io/docs/configuration#transform-objectstring-pathtotransformer--pathtotransformer-object
    '^.+\\.(js|jsx|ts|tsx)$': ['babel-jest', { presets: ['next/babel'] }],
  },
  transformIgnorePatterns: [
    '/node_modules/',
    '^.+\\.module\\.(css|sass|scss)$',
  ],
}

يمكنك معرفة المزيد حول كل خيار تكوين في وثائق Jest. نوصي أيضًا بمراجعة تكوين next/jest لمعرفة كيفية تكوين Next.js لـ Jest.

التعامل مع ملفات الأنماط واستيراد الصور

لا يتم استخدام ملفات الأنماط والصور في الاختبارات ولكن استيرادها قد يتسبب في أخطاء، لذا يجب محاكاتها.

قم بإنشاء ملفات المحاكاة المشار إليها في التكوين أعلاه - fileMock.js وstyleMock.js - داخل دليل __mocks__:

__mocks__/fileMock.js
module.exports = 'test-file-stub'
__mocks__/styleMock.js
module.exports = {}

لمزيد من المعلومات حول التعامل مع الأصول الثابتة، يرجى الرجوع إلى وثائق Jest.

التعامل مع الخطوط

للتعامل مع الخطوط، قم بإنشاء ملف nextFontMock.js داخل دليل __mocks__، وأضف التكوين التالي:

__mocks__/nextFontMock.js
module.exports = new Proxy(
  {},
  {
    get: function getter() {
      return () => ({
        className: 'className',
        variable: 'variable',
        style: { fontFamily: 'fontFamily' },
      })
    },
  }
)

اختياري: التعامل مع الاستيراد المطلق وأسماء مسار الوحدات

إذا كان مشروعك يستخدم أسماء مسار الوحدات (Module Path Aliases)، فستحتاج إلى تكوين Jest لحل عمليات الاستيراد عن طريق مطابقة خيار paths في ملف jsconfig.json مع خيار moduleNameMapper في ملف jest.config.js. على سبيل المثال:

tsconfig.json أو jsconfig.json
{
  "compilerOptions": {
    "module": "esnext",
    "moduleResolution": "bundler",
    "baseUrl": "./",
    "paths": {
      "@/components/*": ["components/*"]
    }
  }
}
jest.config.js
moduleNameMapper: {
  // ...
  '^@/components/(.*)$': '<rootDir>/components/$1',
}

اختياري: توسيع Jest مع مطابقات مخصصة

يتضمن @testing-library/jest-dom مجموعة من المطابقات المخصصة الملائمة مثل .toBeInTheDocument() مما يجعل كتابة الاختبارات أسهل. يمكنك استيراد المطابقات المخصصة لكل اختبار عن طريق إضافة الخيار التالي إلى ملف تكوين Jest:

setupFilesAfterEnv: ['<rootDir>/jest.setup.ts']
setupFilesAfterEnv: ['<rootDir>/jest.setup.js']

ثم، داخل jest.setup.ts، أضف الاستيراد التالي:

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

معلومة مفيدة: تمت إزالة extend-expect في الإصدار v6.0، لذا إذا كنت تستخدم @testing-library/jest-dom قبل الإصدار 6، فستحتاج إلى استيراد @testing-library/jest-dom/extend-expect بدلاً من ذلك.

إذا كنت بحاجة إلى إضافة المزيد من خيارات الإعداد قبل كل اختبار، يمكنك إضافتها إلى ملف jest.setup.js أعلاه.

أضف سكريبت اختبار إلى package.json:

أخيرًا، أضف سكريبت اختبار Jest إلى ملف package.json الخاص بك:

package.json
{
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "test": "jest",
    "test:watch": "jest --watch"
  }
}

سيقوم jest --watch بإعادة تشغيل الاختبارات عند تغيير ملف. لمزيد من خيارات سطر أوامر Jest، يرجى الرجوع إلى وثائق Jest.

إنشاء أول اختبار لك:

مشروعك جاهز الآن لتشغيل الاختبارات. قم بإنشاء مجلد يسمى __tests__ في دليل الجذر لمشروعك.

على سبيل المثال، يمكننا إضافة اختبار للتحقق مما إذا كان المكون <Home /> يقوم بعرض عنوان بنجاح:

export default function Home() {
  return <h1>Home</h1>
}
__tests__/index.test.js
import '@testing-library/jest-dom'
import { render, screen } from '@testing-library/react'
import Home from '../pages/index'

describe('Home', () => {
  it('renders a heading', () => {
    render(<Home />)

    const heading = screen.getByRole('heading', { level: 1 })

    expect(heading).toBeInTheDocument()
  })
})

اختياريًا، أضف اختبار لقطة (snapshot test) لتتبع أي تغييرات غير متوقعة في المكون الخاص بك:

__tests__/snapshot.js
import { render } from '@testing-library/react'
import Home from '../pages/index'

it('renders homepage unchanged', () => {
  const { container } = render(<Home />)
  expect(container).toMatchSnapshot()
})

معلومة مفيدة: لا يجب تضمين ملفات الاختبار داخل مسار الصفحات (Pages Router) لأن أي ملفات داخل مسار الصفحات تعتبر مسارات.

تشغيل الاختبارات الخاصة بك

ثم، قم بتشغيل الأمر التالي لتشغيل اختباراتك:

Terminal
npm run test
# أو
yarn test
# أو
pnpm test

موارد إضافية

لمزيد من القراءة، قد تجد هذه الموارد مفيدة: