Next.js: اختبارات الوحدة والتكامل

آخر تحديث: 2026-08-26

الاختبار ليس اختياريا — إنه "الوسادة الهوائية" لتطبيقات مستوى الإنتاج: لا تحتاجها يوميا، لكنها قد تنقذ حياتك عندما تحتاجها حقا.

1. ما ستتعلمه



2. القصة الحقيقية لمهندس Full-Stack

(1) نقطة الألم: عشية الإطلاق، تسبب فراغ واحد في تعطيل صفحة الدفع

تعمل Alice كمهندسة Full-Stack في منصة تجارة إلكترونية تخدم سوق الشرق الأوسط. تعالج المنصة أكثر من 50,000 طلب يوميا، ويحافظ الفريق على دورة إصدار مرتين في الأسبوع.

خلال إطلاق الجمعة الماضية، تسبب تغيير يبدو غير ضار — إضافة فراغ إضافي إلى دالة تنسيق السعر في مكون OrderSummary — في عرض مبالغ الدفع للمستخدمين السعوديين بفاصلة عشرية إضافية. ورغم أن البيانات نفسها لم تكن خاطئة فعليا، تلقت خدمة العملاء أكثر من 200 شكوى، وألغى ثلاثة مستخدمين طلباتهم نتيجة لذلك.

والأسوأ من ذلك:

كانت Alice مصممة: كان عليها إنشاء نظام اختبار لمنع حدوث هذا النوع من المشاكل مرة أخرى.

(2) حل Vitest + Testing Library

أدخلت Alice مجموعة اختبارات Vitest في المشروع:

BASH
npm install -D vitest @testing-library/react @testing-library/jest-dom @vitejs/plugin-react msw

ثم أنشأت أول اختبار:

TSX
import { render, screen } from '@testing-library/react'
import { OrderSummary } from './OrderSummary'

it('displays formatted price correctly', () => {
  render(<OrderSummary total={99.99} currency="SAR" />)
  expect(screen.getByText(/99\.99/)).toBeInTheDocument()
})

(3) النتائج

البعد قبل بعد
تغطية الكود < 5% > 75%
اختبار الانحدار قبل الإصدار لا يوجد يعمل تلقائيا في 5 دقائق
معدل الأخطاء في الإنتاج 8–12 شهريا 0–2 شهريا
الثقة في تقديم ميزات جديدة منخفضة مرتفعة


3. إعداد بيئة اختبار Vitest

Vitest هو إطار الاختبار الأصلي لنظام Vite البيئي ومتوافق بشكل أصلي مع Next.js 16 (الذي يستخدم Turbopack و Vite في الخلفية).

100%
graph TB
    A[vitest.config.ts] --> B[React Plugin<br/>@vitejs/plugin-react]
    A --> C[Test Globals<br/>globals:true]
    A --> D[Environment<br/>jsdom]
    A --> E[Setup Files<br/>setup-test.ts]
    E --> F[jest-dom Matcher]
    E --> G[MSW Start]
    
    style A fill:#cce5ff
    style F fill:#d4edda
    style G fill:#d4edda
ملف الإعداد الغرض الخيارات الرئيسية
vitest.config.ts الإعداد الرئيسي للاختبار environment: 'jsdom' محاكاة المتصفح
setup-test.ts التهيئة العامة استيراد jest-dom، بدء خدمة MSW
tsconfig.json دعم الأنواع types: ['vitest/globals']

(1) إعداد vitest.config.ts

TS
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
import path from 'path'

export default defineConfig({
  plugins: [react()],
  test: {
    environment: 'jsdom',
    globals: true,
    setupFiles: './src/__tests__/setup-test.ts',
    include: ['src/**/*.{test,spec}.{ts,tsx}'],
    coverage: {
      provider: 'v8',
      reporter: ['text', 'lcov'],
      thresholds: {
        statements: 70,
        branches: 60,
        functions: 70,
        lines: 70
      }
    }
  },
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src')
    }
  }
})

(2) ملف الإعداد العام

TS
// src/__tests__/setup-test.ts
import '@testing-library/jest-dom/vitest'
import { cleanup } from '@testing-library/react'
import { afterEach, vi } from 'vitest'

afterEach(() => {
  cleanup()
})

vi.mock('next/navigation', () => ({
  useRouter: () => ({
    push: vi.fn(),
    replace: vi.fn(),
    refresh: vi.fn(),
    back: vi.fn(),
    forward: vi.fn()
  }),
  usePathname: () => '/',
  useSearchParams: () => new URLSearchParams()
}))

vi.mock('react-dom', async () => {
  const actual = await vi.importActual('react-dom')
  return { ...actual, useFormState: vi.fn() }
})

▶ مثال: التحقق من تشغيل Vitest

TSX
// src/__tests__/basic.test.ts
import { render, screen } from '@testing-library/react'

function Hello({ name }: { name: string }) {
  return <h1>Hello, {name}!</h1>
}

it('renders hello message', () => {
  render(<Hello name="Alice" />)
  expect(screen.getByText('Hello, Alice!')).toBeInTheDocument()
})
BASH
npx vitest run
💻 المخرجات:

TEXT 📖 للعرض فقط
 ✓ src/__tests__/basic.test.ts (1 test) 12ms

 Test Files  1 passed (1)
      Tests  1 passed (1)

المخرجات:

TEXT 📖 للعرض فقط
 ✓ src/__tests__/basic.test.ts (1 test) 12ms

 Test Files  1 passed (1)
       Tests  1 passed (1)

▶ مثال: إضافة سكريبت اختبار إلى package.json

JSON
{
  "scripts": {
    "test": "vitest run",
    "test:watch": "vitest",
    "test:coverage": "vitest run --coverage"
  }
}

المخرجات:

TEXT 📖 للعرض فقط
JSON structure defining three npm scripts: "test" (vitest run), "test:watch" (vitest), and "test:coverage" (vitest run --coverage).


4. اختبار المكونات باستخدام @testing-library/react

الفلسفة الأساسية لـ Testing Library: اختبر ما يراه المستخدمون ويتفاعلون معه، بدلا من تفاصيل التنفيذ.

(1) استعلامات "render" و "screen"

100%
graph LR
    A[render المكونات] --> B[screen البحث]
    B --> C{نوع الاستعلام}
    C --> D[getByText النص]
    C --> E[getByRole الدلالي]
    C --> F[getByTestId معرف الاختبار]
    C --> G[getByPlaceholderText العنصر النائب]
    
    D --> H[التأكيد expect]
    E --> H
    F --> H
    G --> H
طريقة الاستعلام السيناريوهات المناسبة مثال
getByText المحتوى النصي getByText('Submit Order')
getByRole العناصر الدلالية getByRole('button', { name: /Submit/i })
getByPlaceholderText تلميح حقل الإدخال getByPlaceholderText('Enter email')
getByTestId عنصر غير دلالي getByTestId('order-total')
queryByText تأكيد عدم الوجود expect(queryByText('Error')).not.toBeInTheDocument()

(2) اختبار مكونات الخادم (العرض المتزامن)

مكونات الخادم لا تتطلب JavaScript من جانب العميل؛ يمكنك تأكيد مخرجات HTML مباشرة أثناء الاختبار:

TSX
// src/app/products/page.tsx
async function ProductsPage() {
  const res = await fetch('https://api.example.com/products')
  const products = await res.json()
  
  return (
    <ul>
      {products.map((p: { id: number; name: string; price: number }) => (
        <li key={p.id} data-testid="product-item">
          {p.name} — ${p.price}
        </li>
      ))}
    </ul>
  )
}

export default ProductsPage
TSX
// src/__tests__/products-page.test.tsx
import { render, screen } from '@testing-library/react'
import ProductsPage from '@/app/products/page'

// محاكاة fetch العامة
global.fetch = vi.fn().mockResolvedValue({
  json: () => Promise.resolve([
    { id: 1, name: 'iPhone 16', price: 999 },
    { id: 2, name: 'Samsung S26', price: 899 }
  ])
})

it('renders product list from server', async () => {
  const page = await ProductsPage()
  render(page)
  
  expect(screen.getByText('iPhone 16')).toBeInTheDocument()
  expect(screen.getByText('Samsung S26')).toBeInTheDocument()
  expect(screen.getAllByTestId('product-item')).toHaveLength(2)
})

▶ مثال: اختبار مكونات العميل (بما في ذلك تفاعلات المستخدم)

المخرجات:

TEXT 📖 للعرض فقط
Renders the Component component UI.
TSX
// src/components/Counter.tsx
'use client'

import { useState } from 'react'

export function Counter({ initial = 0 }) {
  const [count, setCount] = useState(initial)
  
  return (
    <div>
      <p data-testid="count">Count: {count}</p>
      <button onClick={() => setCount(c => c + 1)}>Increment</button>
      <button onClick={() => setCount(c => c - 1)}>Decrement</button>
    </div>
  )
}

المخرجات:

TEXT 📖 للعرض فقط
An interactive component with state management.
Visible text: Count: {count}
TSX
// src/__tests__/counter.test.tsx
import { render, screen, fireEvent } from '@testing-library/react'
import { Counter } from '@/components/Counter'

describe('Counter', () => {
  it('renders with initial value', () => {
    render(<Counter initial={5} />)
    expect(screen.getByTestId('count')).toHaveTextContent('Count: 5')
  })
  
  it('increments on button click', () => {
    render(<Counter initial={0} />)
    fireEvent.click(screen.getByText('Increment'))
    expect(screen.getByTestId('count')).toHaveTextContent('Count: 1')
  })
  
  it('decrements on button click', () => {
    render(<Counter initial={10} />)
    fireEvent.click(screen.getByText('Decrement'))
    expect(screen.getByTestId('count')).toHaveTextContent('Count: 9')
  })
})


5. المطابقات المخصصة في jest-dom

توفر jest-dom مطابقات تأكيد DOM دلالية، مما يجعل كود الاختبار أشبه باللغة الطبيعية.

المطابق الوظيفة مثال
toBeInTheDocument() العناصر موجودة في DOM expect(el).toBeInTheDocument()
toHaveTextContent(text) مطابقة المحتوى النصي expect(el).toHaveTextContent('Hello')
toBeVisible() العنصر مرئي expect(el).toBeVisible()
toBeDisabled() الزر معطل expect(btn).toBeDisabled()
toHaveClass(cls) مطابقة فئة CSS expect(el).toHaveClass('active')
toHaveAttribute(attr) مطابقة السمة expect(input).toHaveAttribute('type', 'email')
toHaveValue(val) مطابقة قيمة النموذج expect(input).toHaveValue('test@example.com')

▶ مثال: اختبار التحقق من النموذج

المخرجات:

TEXT 📖 للعرض فقط
Renders the Component component UI.
TSX
import { render, screen, fireEvent } from '@testing-library/react'
import { LoginForm } from '@/components/LoginForm'

describe('LoginForm validation', () => {
  it('shows error on empty email', () => {
    render(<LoginForm />)
    fireEvent.click(screen.getByRole('button', { name: /Log In/i }))
    expect(screen.getByText(/Please enter your email address/i)).toBeInTheDocument()
    expect(screen.getByText(/Please enter your email address/i)).toBeVisible()
  })
  
  it('disables submit while loading', () => {
    render(<LoginForm />)
    fireEvent.change(screen.getByPlaceholderText('Enter your email address'), {
      target: { value: 'alice@example.com' }
    })
    fireEvent.click(screen.getByRole('button', { name: /Log In/i }))
    expect(screen.getByRole('button', { name: /Logging in.../i })).toBeDisabled()
  })
  
  it('clears error after valid input', () => {
    render(<LoginForm />)
    fireEvent.click(screen.getByRole('button', { name: /Log In/i }))
    expect(screen.getByText(/Please enter your email address/i)).toBeInTheDocument()
    fireEvent.change(screen.getByPlaceholderText('Enter your email address'), {
      target: { value: 'alice@example.com' }
    })
    expect(screen.queryByText(/Please enter your email address/i)).not.toBeInTheDocument()
  })
})

المخرجات:

TEXT 📖 للعرض فقط
Renders the ▶ Example: Form Validation Test component UI as described in the section.


6. اختبار Server Actions (محاكاة Prisma + revalidatePath)

تحتاج Server Actions إلى محاكاة عمليات قاعدة البيانات ودوال إبطال ذاكرة التخزين المؤقت.

100%
graph TB
    A[اختبار Server Action] --> B[محاكاة Prisma Client]
    A --> C[محاكاة revalidatePath]
    A --> D[محاكاة redirect]
    B --> E[إرجاع بيانات محاكاة]
    C --> F[التحقق من عدد مرات الاستدعاء/المعلمات]
    D --> G[التحقق من مسار إعادة التوجيه]
    
    style A fill:#cce5ff
    style B fill:#d4edda
    style C fill:#fff3cd

(1) أداة محاكاة Prisma

TS
// src/__tests__/utils/mock-prisma.ts
import { vi } from 'vitest'

export function createMockPrisma() {
  return {
    task: {
      findMany: vi.fn().mockResolvedValue([
        { id: '1', title: 'Test Task', status: 'TODO', projectId: 'p1' }
      ]),
      create: vi.fn().mockImplementation(({ data }) => Promise.resolve({
        id: 'new-id',
        ...data,
        createdAt: new Date()
      })),
      update: vi.fn().mockImplementation(({ data }) => Promise.resolve(data)),
      delete: vi.fn().mockResolvedValue({ id: 'deleted-id' })
    },
    project: {
      findUnique: vi.fn().mockResolvedValue({
        id: 'p1',
        name: 'Test Project',
        tasks: []
      })
    },
    $transaction: vi.fn().mockImplementation((cb) => cb(createMockPrisma()))
  } as any
}

(2) مثال على اختبار Server Action

TS
// src/actions/task.ts
'use server'

import { prisma } from '@/lib/prisma'
import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'
import { z } from 'zod'

const taskSchema = z.object({
  title: z.string().min(1, 'The title cannot be left blank.'),
  projectId: z.string().min(1),
  status: z.enum(['TODO', 'IN_PROGRESS', 'DONE'])
})

export async function createTask(formData: FormData) {
  const data = Object.fromEntries(formData)
  const parsed = taskSchema.safeParse(data)
  
  if (!parsed.success) {
    return { error: parsed.error.flatten().fieldErrors }
  }
  
  await prisma.task.create({ data: parsed.data })
  revalidatePath(`/projects/${parsed.data.projectId}`)
  redirect(`/projects/${parsed.data.projectId}`)
}

▶ مثال: اختبار Server Action كامل

المخرجات:

TEXT 📖 للعرض فقط
Validates input with Zod schema before processing.
Writes to the database and refreshes the affected page cache.
Returns error object if validation or processing fails.
TS
// src/__tests__/actions/task.test.ts
import { createTask } from '@/actions/task'
import { createMockPrisma } from '../utils/mock-prisma'
import { vi, describe, it, expect, beforeEach } from 'vitest'

vi.mock('@/lib/prisma', () => ({ prisma: createMockPrisma() }))
vi.mock('next/cache', () => ({ revalidatePath: vi.fn() }))
vi.mock('next/navigation', () => ({ redirect: vi.fn() }))

describe('createTask', () => {
  beforeEach(() => {
    vi.clearAllMocks()
  })
  
  it('creates a task successfully', async () => {
    const formData = new FormData()
    formData.append('title', 'Writing Test Documentation')
    formData.append('projectId', 'p1')
    formData.append('status', 'TODO')
    
    const result = await createTask(formData)
    expect(result).toBeUndefined()
  })
  
  it('returns validation error for empty title', async () => {
    const formData = new FormData()
    formData.append('title', '')
    formData.append('projectId', 'p1')
    formData.append('status', 'TODO')
    
    const result = await createTask(formData)
    expect(result).toHaveProperty('error')
    expect(result.error.title).toBeDefined()
  })
  
  it('calls revalidatePath after creation', async () => {
    const formData = new FormData()
    formData.append('title', 'New Task')
    formData.append('projectId', 'p1')
    formData.append('status', 'TODO')
    
    await createTask(formData)
    const { revalidatePath } = await import('next/cache')
    expect(revalidatePath).toHaveBeenCalledWith('/projects/p1')
  })
})

المخرجات:

TEXT 📖 للعرض فقط
Creates a new database record.


7. محاكاة API جانب الخادم باستخدام MSW

يعترض MSW (Mock Service Worker) طلبات الشبكة، مما يتيح لك اختبار منطق استرجاع البيانات دون الحاجة إلى تشغيل الخادم الخلفي الفعلي.

100%
graph LR
    A[المكون يبدأ fetch] --> B[MSW Service Worker]
    B --> C{مطابقة الطلب}
    C -->|مطابقة المعالج| D[إرجاع بيانات محاكاة]
    C -->|عدم تطابق| E[التمرير إلى الشبكة الفعلية]
    
    style B fill:#cce5ff
    style D fill:#d4edda
المفهوم الوصف مثال
Handler معالج الطلب http.get('/api/products', resolver)
Resolver إرجاع استجابة محاكاة return HttpResponse.json([...])
Server خادم محاكاة Node.js setupServer(...handlers)
Browser MSW في المتصفح setupWorker(...handlers)

(1) تعريف معالجات المحاكاة

TS
// src/mocks/handlers.ts
import { http, HttpResponse } from 'msw'

const API_BASE = 'https://api.example.com'

export const handlers = [
  // GET /api/products
  http.get(`${API_BASE}/products`, () => {
    return HttpResponse.json([
      { id: 1, name: 'iPhone 16', price: 999, category: 'Electronics' },
      { id: 2, name: 'Samsung S26', price: 899, category: 'Electronics' }
    ])
  }),
  
  // POST /api/orders
  http.post(`${API_BASE}/orders`, async ({ request }) => {
    const body = await request.json()
    return HttpResponse.json({
      id: 'order-123',
      ...body as any,
      status: 'confirmed',
      createdAt: new Date().toISOString()
    })
  }),
  
  // GET /api/users/:id
  http.get(`${API_BASE}/users/:id`, ({ params }) => {
    return HttpResponse.json({
      id: params.id,
      name: 'Alice Wang',
      email: 'alice@example.com',
      role: 'admin'
    })
  }),
  
  // خطأ 500 لاختبار حدود الأخطاء
  http.get(`${API_BASE}/errors/internal`, () => {
    return HttpResponse.json(
      { error: 'Internal Server Error' },
      { status: 500 }
    )
  })
]

(2) الدمج في إعداد الاختبار

TS
// src/mocks/server.ts
import { setupServer } from 'msw/node'
import { handlers } from './handlers'

export const server = setupServer(...handlers)
TS
// src/__tests__/setup-test.ts (النسخة الكاملة)
import '@testing-library/jest-dom/vitest'
import { cleanup } from '@testing-library/react'
import { afterEach, afterAll, beforeAll, vi } from 'vitest'
import { server } from '@/mocks/server'

beforeAll(() => server.listen({ onUnhandledRequest: 'warn' }))
afterEach(() => { cleanup(); server.resetHandlers() })
afterAll(() => server.close())

// محاكاة next/navigation
vi.mock('next/navigation', () => ({
  useRouter: () => ({
    push: vi.fn(), replace: vi.fn(),
    refresh: vi.fn(), back: vi.fn(), forward: vi.fn()
  }),
  usePathname: () => '/',
  useSearchParams: () => new URLSearchParams()
}))

▶ مثال: اختبار اعتراض API باستخدام MSW

المخرجات:

TEXT 📖 للعرض فقط
TypeScript module executes successfully.
TSX
// src/__tests__/api-integration.test.tsx
import { render, screen, waitFor } from '@testing-library/react'
import { http, HttpResponse } from 'msw'
import { server } from '@/mocks/server'
import ProductsPage from '@/app/products/page'

describe('ProductsPage with MSW', () => {
  it('renders products from mocked API', async () => {
    const page = await ProductsPage()
    render(page)
    
    expect(screen.getByText('iPhone 16')).toBeInTheDocument()
    expect(screen.getByText('Samsung S26')).toBeInTheDocument()
  })
  
  it('handles empty product list', async () => {
    server.use(
      http.get('https://api.example.com/products', () => {
        return HttpResponse.json([])
      })
    )
    
    const page = await ProductsPage()
    render(page)
    await waitFor(() => {
      expect(screen.queryByTestId('product-item')).not.toBeInTheDocument()
    })
  })
  
  it('shows error on API failure', async () => {
    server.use(
      http.get('https://api.example.com/products', () => {
        return HttpResponse.json(
          { error: 'Service unavailable' },
          { status: 503 }
        )
      })
    )
    
    await expect(ProductsPage()).rejects.toThrow()
  })
})

المخرجات:

TEXT 📖 للعرض فقط
Renders the ▶ Example: Testing the MSW Interception API component UI as described in the section.


8. مثال كامل: اختبار Full-Stack لتطبيق Todo

TSX
// src/__tests__/todo-comprehensive.test.ts
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
import { http, HttpResponse } from 'msw'
import { server } from '@/mocks/server'
import { createMockPrisma } from './utils/mock-prisma'
import { describe, it, expect, vi, beforeEach } from 'vitest'

// ============================================
// اختبار شامل: مكونات تطبيق Todo + Action + API
// ============================================

// --- 1. محاكاة الاعتماديات ---
vi.mock('@/lib/prisma', () => ({ prisma: createMockPrisma() }))
vi.mock('next/cache', () => ({ revalidatePath: vi.fn() }))
vi.mock('next/navigation', () => ({
  redirect: vi.fn(),
  useRouter: () => ({ push: vi.fn(), refresh: vi.fn() })
}))

// --- 2. مكونات Todo ---
function TodoItem({ id, title, done, onToggle }: {
  id: string; title: string; done: boolean
  onToggle: (id: string) => void
}) {
  return (
    <div data-testid="todo-item">
      <span style={{ textDecoration: done ? 'line-through' : 'none' }}>
        {title}
      </span>
      <button onClick={() => onToggle(id)}>
        {done ? 'Undo' : 'Done'}
      </button>
    </div>
  )
}

function TodoList({ todos }: { todos: Array<{ id: string; title: string; done: boolean }> }) {
  return (
    <div>
      {todos.map(t => (
        <TodoItem key={t.id} {...t} onToggle={(id) => {
          const idx = todos.findIndex(t => t.id === id)
          todos[idx].done = !todos[idx].done
        }} />
      ))}
    </div>
  )
}

// --- 3. حالات الاختبار ---
describe('Todo App', () => {
  const mockTodos = [
    { id: '1', title: 'Learn Next.js testing', done: false },
    { id: '2', title: 'Write unit tests', done: true },
    { id: '3', title: 'Set up CI pipeline', done: false }
  ]
  
  it('renders all todos', () => {
    render(<TodoList todos={mockTodos} />)
    expect(screen.getAllByTestId('todo-item')).toHaveLength(3)
    expect(screen.getByText('Learn Next.js testing')).toBeInTheDocument()
  })
  
  it('shows strikethrough for done items', () => {
    render(<TodoList todos={mockTodos} />)
    const doneItem = screen.getByText('Write unit tests')
    expect(doneItem).toHaveStyle('text-decoration: line-through')
  })
  
  it('toggles todo on button click', () => {
    render(<TodoList todos={mockTodos} />)
    const buttons = screen.getAllByRole('button')
    fireEvent.click(buttons[0])
    expect(buttons[0]).toHaveTextContent('Undo')
  })
  
  it('has correct button labels', () => {
    render(<TodoList todos={mockTodos} />)
    const buttons = screen.getAllByRole('button')
    expect(buttons[0]).toHaveTextContent('Done')
    expect(buttons[1]).toHaveTextContent('Undo')
  })
})

// --- 4. اختبار MSW API ---
describe('Todo API Mock', () => {
  it('fetch todos from mocked API', async () => {
    server.use(
      http.get('https://api.example.com/todos', () => {
        return HttpResponse.json(mockTodos)
      })
    )
    
    const res = await fetch('https://api.example.com/todos')
    const data = await res.json()
    expect(data).toHaveLength(3)
    expect(data[0].title).toBe('Learn Next.js testing')
  })
  
  it('handles API error gracefully', async () => {
    server.use(
      http.get('https://api.example.com/todos', () => {
        return HttpResponse.json(null, { status: 500 })
      })
    )
    
    const res = await fetch('https://api.example.com/todos')
    expect(res.status).toBe(500)
  })
})
💻 المخرجات:

TEXT 📖 للعرض فقط
 ✓ src/__tests__/todo-comprehensive.test.ts (7 tests) 45ms

 Test Files  1 passed (1)
      Tests  7 passed (7)

❓ أسئلة شائعة

س ما الفرق بين Vitest و Jest؟
ج يتشارك Vitest إعداداته ونظامه الإضافي مع Vite، ويبدأ التشغيل أسرع بـ 10–20 مرة من Jest (مع دعم ESM الأصلي)، ويمتلك واجهة برمجة متوافقة نحويا مع Jest (تدعم واجهة globals API كلا من expect/describe/it). يستخدم Next.js 16 Turbopack (جزء من نظام Vite البيئي) في الخلفية، لذا فإن Vitest هو الخيار الأكثر طبيعية.
س كيف أختبر مكون خادم يستخدم cookies() أو headers()؟
ج تعتمد هذه الدوال على سياق طلب Next.js، والذي يحتاج إلى محاكاة في الاختبارات. نوصي باستخراج منطق استرجاع البيانات إلى Server Action أو طبقة API منفصلة، ثم اختبار عرض المكون ومنطق الأعمال بشكل منفصل.
س ما الفرق بين MSW والمحاكاة اليدوية لـ fetch؟
ج يعترض MSW طلبات الشبكة على مستوى Service Worker، مما يتيح لك اختبار سلاسل استدعاء fetch الحقيقية دون إجراء أي تغييرات على كود المكون. المحاكاة اليدوية لـ global.fetch بسيطة، لكنها لا تستطيع التعامل مع مطابقة الطلبات المعقدة أو تسلسل الاستجابة أو سيناريوهات الأخطاء.
س ما هو الحد المعقول لتغطية الاختبار؟
ج نوصي بنهج تدريجي: حدد الهدف عند 50% للمرحلة 1 (المكونات الأساسية + server actions)، وارفعه إلى 70% للمرحلة 2 (تغطية الفروع والحالات الحدية)، واستهدف 80%+ لمشاريع الإنتاج. تذكر أن التغطية ليست الهدف النهائي؛ المسارات الحرجة (الدفع/تسجيل الدخول/كتابة البيانات) يجب أن تحصل على تغطية 100%.
س كيف أختبر نموذجا يستخدم useActionState؟
ج يعتمد useActionState على useFormState في React 19، لذا تحتاج إلى محاكاة useFormState في react-dom أثناء الاختبار. نوصي باستخدام fireEvent.submit لتشغيل إرسال النموذج، ثم تأكيد حالة واجهة المستخدم بعد الإرسال (رسائل النجاح/الخطأ).
س ما الفرق بين fireEvent و userEvent في Testing Library؟
ج يقوم fireEvent بتشغيل أحداث DOM مباشرة (مثل النقر)، بينما يحاكي userEvent تفاعلات مستخدم أكثر واقعية (مثل إدخال لوحة المفاتيح وإدارة التركيز). نوصي باستخدام userEvent لاختبارات الإنتاج (لأنه يحاكي تجربة المستخدم بشكل أقرب) و fireEvent لاختبارات الوحدة (لأنه أخف وزنا).

📖 ملخص


📝 تمارين

  1. تمرين أساسي (⭐): أنشئ vitest.config.ts يتضمن React plugin وبيئة jsdom ومسارا مستعارا لـ @/، ثم اكتب أبسط اختبار render(

    Hello
    ) للتحقق من أن مطابق Jest-DOM يعمل بشكل صحيح.

  2. تمرين متقدم (⭐⭐): اكتب اختبارات شاملة لـ Server Actions في مشروعك (مثل createUser أو submitOrder): قم بمحاكاة طريقة create في Prisma، وتحقق من استدعاء revalidatePath، واختبر استجابة الخطأ عند فشل التحقق من Zod.

  3. تحدّ (⭐⭐⭐): استخدم MSW لبناء ثلاثة معالجات محاكاة (GET /api/tasks، POST /api/tasks، DELETE /api/tasks/:id)، ثم اكتب 8 حالات اختبار على الأقل لمكون TaskList، تتضمن وظائف استرجاع البيانات وإنشائها وحذفها (بما في ذلك التحميل الأولي والقائمة الفارغة ومعالجة الأخطاء).

Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%