Vitest و React Testing Library مع Next.js 15: الدليل الشامل لاختبارات الوحدة في 2026

اختبر بثقة. Vitest هو إطار الاختبارات فائق السرعة المدعوم بـ Vite الذي حل محل Jest في معظم المشاريع الحديثة بحلول 2026. عند دمجه مع React Testing Library، يوفر تجربة اختبار بديهية وسريعة وموثوقة. في هذا الدليل، ستتعلم كيفية اختبار كل طبقة من تطبيق Next.js 15 الخاص بك.
ما ستتعلمه
بنهاية هذا الدليل، ستتمكن من:
- إعداد Vitest في مشروع Next.js 15 مع App Router
- كتابة اختبارات المكونات باستخدام React Testing Library
- اختبار الـ hooks المخصصة باستخدام
renderHook - عمل Mock لـ Server Components وServer Actions
- اختبار مسارات API (Route Handlers)
- قياس وتحسين تغطية الكود
- دمج الاختبارات في خط أنابيب CI/CD
- تطبيق أفضل الممارسات لاختبارات قابلة للصيانة
المتطلبات الأساسية
قبل البدء، تأكد من توفر:
- Node.js 20+ مثبت (
node --version) - خبرة في TypeScript وReact
- معرفة بـ Next.js 15 (App Router، Server Components)
- محرر أكواد — يُنصح بـ VS Code أو Cursor
- فهم أساسي لـ اختبار البرمجيات (assertions، mocks)
لماذا Vitest بدلاً من Jest؟
في 2026، أصبح Vitest المعيار القياسي للاختبارات في نظام JavaScript البيئي. إليك السبب:
| المعيار | Jest | Vitest |
|---|---|---|
| السرعة | بطيء (تحويل CJS) | فائق السرعة (ESM أصلي عبر Vite) |
| الإعداد | معقد مع Next.js | بسيط مع next/vitest |
| ESM | دعم جزئي | دعم أصلي كامل |
| إعادة التحميل | لا | وضع مراقبة ذكي |
| API | خاص | متوافق مع Jest (ترحيل سهل) |
| TypeScript | يحتاج ts-jest | دعم أصلي |
واجهة Vitest متوافقة عمداً مع Jest، مما يعني أنك إذا كنت تعرف Jest، فأنت تعرف Vitest بالفعل.
الخطوة 1: إنشاء مشروع Next.js
أنشئ مشروع Next.js 15 جديد:
npx create-next-app@latest my-tested-app --typescript --tailwind --app --src-dir
cd my-tested-appتحقق من أن كل شيء يعمل:
npm run devالخطوة 2: تثبيت Vitest و React Testing Library
ثبّت حزم الاختبار:
npm install -D vitest @vitejs/plugin-react @testing-library/react @testing-library/jest-dom @testing-library/user-event jsdomإليك ما تفعله كل حزمة:
| الحزمة | الدور |
|---|---|
vitest | إطار الاختبارات |
@vitejs/plugin-react | دعم JSX/TSX في Vitest |
@testing-library/react | أدوات لاختبار مكونات React |
@testing-library/jest-dom | matchers مخصصة للـ DOM (toBeInTheDocument، إلخ.) |
@testing-library/user-event | محاكاة واقعية لتفاعلات المستخدم |
jsdom | بيئة DOM لـ Node.js |
الخطوة 3: إعداد Vitest
أنشئ ملف vitest.config.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/test/setup.ts"],
include: ["src/**/*.{test,spec}.{ts,tsx}"],
coverage: {
provider: "v8",
reporter: ["text", "json", "html"],
include: ["src/**/*.{ts,tsx}"],
exclude: [
"src/**/*.{test,spec}.{ts,tsx}",
"src/**/*.d.ts",
"src/test/**",
],
},
},
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
});أنشئ ملف الإعداد src/test/setup.ts:
import "@testing-library/jest-dom/vitest";
import { cleanup } from "@testing-library/react";
import { afterEach } from "vitest";
// تنظيف تلقائي بعد كل اختبار
afterEach(() => {
cleanup();
});أضف سكريبتات الاختبار في package.json:
{
"scripts": {
"test": "vitest",
"test:run": "vitest run",
"test:coverage": "vitest run --coverage",
"test:ui": "vitest --ui"
}
}لوضع الواجهة التفاعلية (اختياري لكن مفيد جداً):
npm install -D @vitest/uiالخطوة 4: إعداد TypeScript للاختبارات
أضف أنواع Vitest في tsconfig.json:
{
"compilerOptions": {
"types": ["vitest/globals", "@testing-library/jest-dom"]
}
}هذا يسمح باستخدام describe وit وexpect وmatchers الخاصة بـ jest-dom بدون استيراد صريح.
الخطوة 5: أول اختبار لمكون
لنُنشئ مكوناً بسيطاً لاختباره. أنشئ src/components/Counter.tsx:
"use client";
import { useState } from "react";
interface CounterProps {
initialCount?: number;
step?: number;
}
export function Counter({ initialCount = 0, step = 1 }: CounterProps) {
const [count, setCount] = useState(initialCount);
return (
<div>
<p data-testid="count-display">العداد: {count}</p>
<button onClick={() => setCount((c) => c + step)}>زيادة</button>
<button onClick={() => setCount((c) => c - step)}>إنقاص</button>
<button onClick={() => setCount(0)}>إعادة تعيين</button>
</div>
);
}الآن اكتب الاختبار في src/components/__tests__/Counter.test.tsx:
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { Counter } from "../Counter";
describe("Counter", () => {
it("يعرض العداد الأولي بقيمة 0", () => {
render(<Counter />);
expect(screen.getByTestId("count-display")).toHaveTextContent("العداد: 0");
});
it("يقبل قيمة أولية مخصصة", () => {
render(<Counter initialCount={10} />);
expect(screen.getByTestId("count-display")).toHaveTextContent("العداد: 10");
});
it("يزيد العداد عند النقر", async () => {
const user = userEvent.setup();
render(<Counter />);
await user.click(screen.getByText("زيادة"));
expect(screen.getByTestId("count-display")).toHaveTextContent("العداد: 1");
});
it("ينقص العداد عند النقر", async () => {
const user = userEvent.setup();
render(<Counter initialCount={5} />);
await user.click(screen.getByText("إنقاص"));
expect(screen.getByTestId("count-display")).toHaveTextContent("العداد: 4");
});
it("يعيد تعيين العداد إلى صفر", async () => {
const user = userEvent.setup();
render(<Counter initialCount={42} />);
await user.click(screen.getByText("إعادة تعيين"));
expect(screen.getByTestId("count-display")).toHaveTextContent("العداد: 0");
});
it("يستخدم خطوة مخصصة", async () => {
const user = userEvent.setup();
render(<Counter step={5} />);
await user.click(screen.getByText("زيادة"));
expect(screen.getByTestId("count-display")).toHaveTextContent("العداد: 5");
await user.click(screen.getByText("زيادة"));
expect(screen.getByTestId("count-display")).toHaveTextContent("العداد: 10");
});
});شغّل الاختبار:
npm testيجب أن ترى جميع الاختبارات تمر بنجاح باللون الأخضر.
الخطوة 6: اختبار مكونات النماذج
النماذج هي جوهر معظم التطبيقات. أنشئ src/components/LoginForm.tsx:
"use client";
import { useState } from "react";
interface LoginFormProps {
onSubmit: (data: { email: string; password: string }) => Promise<void>;
}
export function LoginForm({ onSubmit }: LoginFormProps) {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const [isLoading, setIsLoading] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError("");
if (!email.includes("@")) {
setError("عنوان بريد إلكتروني غير صالح");
return;
}
if (password.length < 8) {
setError("يجب أن تكون كلمة المرور 8 أحرف على الأقل");
return;
}
setIsLoading(true);
try {
await onSubmit({ email, password });
} catch (err) {
setError("فشل تسجيل الدخول. يرجى المحاولة مرة أخرى.");
} finally {
setIsLoading(false);
}
};
return (
<form onSubmit={handleSubmit} aria-label="نموذج تسجيل الدخول">
{error && (
<div role="alert" className="text-red-500">
{error}
</div>
)}
<label htmlFor="email">البريد الإلكتروني</label>
<input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
<label htmlFor="password">كلمة المرور</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
<button type="submit" disabled={isLoading}>
{isLoading ? "جارٍ تسجيل الدخول..." : "تسجيل الدخول"}
</button>
</form>
);
}اختبره في src/components/__tests__/LoginForm.test.tsx:
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { vi } from "vitest";
import { LoginForm } from "../LoginForm";
describe("LoginForm", () => {
const mockOnSubmit = vi.fn();
beforeEach(() => {
mockOnSubmit.mockReset();
});
it("يعرض النموذج بجميع الحقول", () => {
render(<LoginForm onSubmit={mockOnSubmit} />);
expect(screen.getByLabelText("البريد الإلكتروني")).toBeInTheDocument();
expect(screen.getByLabelText("كلمة المرور")).toBeInTheDocument();
expect(screen.getByText("تسجيل الدخول")).toBeInTheDocument();
});
it("يعرض خطأ لبريد إلكتروني غير صالح", async () => {
const user = userEvent.setup();
render(<LoginForm onSubmit={mockOnSubmit} />);
await user.type(screen.getByLabelText("البريد الإلكتروني"), "invalid-email");
await user.type(screen.getByLabelText("كلمة المرور"), "password123");
await user.click(screen.getByText("تسجيل الدخول"));
expect(screen.getByRole("alert")).toHaveTextContent(
"عنوان بريد إلكتروني غير صالح"
);
expect(mockOnSubmit).not.toHaveBeenCalled();
});
it("يعرض خطأ لكلمة مرور قصيرة", async () => {
const user = userEvent.setup();
render(<LoginForm onSubmit={mockOnSubmit} />);
await user.type(screen.getByLabelText("البريد الإلكتروني"), "user@example.com");
await user.type(screen.getByLabelText("كلمة المرور"), "short");
await user.click(screen.getByText("تسجيل الدخول"));
expect(screen.getByRole("alert")).toHaveTextContent(
"يجب أن تكون كلمة المرور 8 أحرف على الأقل"
);
});
it("يرسل النموذج ببيانات صحيحة", async () => {
mockOnSubmit.mockResolvedValue(undefined);
const user = userEvent.setup();
render(<LoginForm onSubmit={mockOnSubmit} />);
await user.type(screen.getByLabelText("البريد الإلكتروني"), "user@example.com");
await user.type(screen.getByLabelText("كلمة المرور"), "password123");
await user.click(screen.getByText("تسجيل الدخول"));
expect(mockOnSubmit).toHaveBeenCalledWith({
email: "user@example.com",
password: "password123",
});
});
it("يعطل الزر أثناء التحميل", async () => {
mockOnSubmit.mockImplementation(
() => new Promise((resolve) => setTimeout(resolve, 1000))
);
const user = userEvent.setup();
render(<LoginForm onSubmit={mockOnSubmit} />);
await user.type(screen.getByLabelText("البريد الإلكتروني"), "user@example.com");
await user.type(screen.getByLabelText("كلمة المرور"), "password123");
await user.click(screen.getByText("تسجيل الدخول"));
expect(screen.getByText("جارٍ تسجيل الدخول...")).toBeDisabled();
});
it("يعرض خطأ عند فشل الإرسال", async () => {
mockOnSubmit.mockRejectedValue(new Error("Network error"));
const user = userEvent.setup();
render(<LoginForm onSubmit={mockOnSubmit} />);
await user.type(screen.getByLabelText("البريد الإلكتروني"), "user@example.com");
await user.type(screen.getByLabelText("كلمة المرور"), "password123");
await user.click(screen.getByText("تسجيل الدخول"));
expect(
await screen.findByText("فشل تسجيل الدخول. يرجى المحاولة مرة أخرى.")
).toBeInTheDocument();
});
});الخطوة 7: اختبار الـ Hooks المخصصة
توفر React Testing Library دالة renderHook لاختبار الـ hooks بشكل منفصل. أنشئ src/hooks/useLocalStorage.ts:
"use client";
import { useState, useEffect } from "react";
export function useLocalStorage<T>(key: string, initialValue: T) {
const [storedValue, setStoredValue] = useState<T>(() => {
if (typeof window === "undefined") return initialValue;
try {
const item = window.localStorage.getItem(key);
return item ? (JSON.parse(item) as T) : initialValue;
} catch {
return initialValue;
}
});
useEffect(() => {
try {
window.localStorage.setItem(key, JSON.stringify(storedValue));
} catch {
console.error(`خطأ في حفظ "${key}" في localStorage`);
}
}, [key, storedValue]);
return [storedValue, setStoredValue] as const;
}اختبره في src/hooks/__tests__/useLocalStorage.test.ts:
import { renderHook, act } from "@testing-library/react";
import { useLocalStorage } from "../useLocalStorage";
describe("useLocalStorage", () => {
beforeEach(() => {
localStorage.clear();
});
it("يرجع القيمة الأولية عندما يكون localStorage فارغاً", () => {
const { result } = renderHook(() => useLocalStorage("theme", "light"));
expect(result.current[0]).toBe("light");
});
it("يقرأ القيمة الموجودة من localStorage", () => {
localStorage.setItem("theme", JSON.stringify("dark"));
const { result } = renderHook(() => useLocalStorage("theme", "light"));
expect(result.current[0]).toBe("dark");
});
it("يحدث القيمة في localStorage", () => {
const { result } = renderHook(() => useLocalStorage("count", 0));
act(() => {
result.current[1](42);
});
expect(result.current[0]).toBe(42);
expect(JSON.parse(localStorage.getItem("count")!)).toBe(42);
});
it("يتعامل مع الكائنات المعقدة", () => {
const initial = { name: "أحمد", age: 30 };
const { result } = renderHook(() => useLocalStorage("user", initial));
act(() => {
result.current[1]({ name: "سارة", age: 25 });
});
expect(result.current[0]).toEqual({ name: "سارة", age: 25 });
});
});الخطوة 8: عمل Mock للوحدات والـ APIs
يوفر Vitest نظام mocking قوي. إليك التقنيات الأكثر شيوعاً.
عمل Mock لوحدة كاملة
import { vi } from "vitest";
// عمل Mock لـ next/navigation
vi.mock("next/navigation", () => ({
useRouter: () => ({
push: vi.fn(),
replace: vi.fn(),
back: vi.fn(),
}),
useSearchParams: () => new URLSearchParams(),
usePathname: () => "/",
}));عمل Mock لاستدعاءات fetch
أنشئ src/lib/api.ts:
export async function fetchUsers() {
const response = await fetch("/api/users");
if (!response.ok) throw new Error("فشل في الجلب");
return response.json();
}اختبر مع mock لـ fetch:
import { vi } from "vitest";
import { fetchUsers } from "../api";
describe("fetchUsers", () => {
it("يرجع المستخدمين", async () => {
const mockUsers = [
{ id: 1, name: "أحمد" },
{ id: 2, name: "سارة" },
];
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: () => Promise.resolve(mockUsers),
});
const users = await fetchUsers();
expect(users).toEqual(mockUsers);
expect(fetch).toHaveBeenCalledWith("/api/users");
});
it("يرمي خطأ عند الفشل", async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: false,
status: 500,
});
await expect(fetchUsers()).rejects.toThrow("فشل في الجلب");
});
});عمل Mock باستخدام vi.spyOn
import * as api from "../api";
it("يتجسس على استدعاء دالة", async () => {
const spy = vi.spyOn(api, "fetchUsers").mockResolvedValue([]);
// ... الكود الذي يستدعي fetchUsers
expect(spy).toHaveBeenCalledTimes(1);
spy.mockRestore();
});الخطوة 9: اختبار المكونات مع بيانات غير متزامنة
أنشئ مكوناً يجلب البيانات. src/components/UserList.tsx:
"use client";
import { useState, useEffect } from "react";
interface User {
id: number;
name: string;
email: string;
}
export function UserList() {
const [users, setUsers] = useState<User[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
fetch("/api/users")
.then((res) => {
if (!res.ok) throw new Error("خطأ في الخادم");
return res.json();
})
.then(setUsers)
.catch((err) => setError(err.message))
.finally(() => setIsLoading(false));
}, []);
if (isLoading) return <p>جارٍ التحميل...</p>;
if (error) return <p role="alert">خطأ: {error}</p>;
return (
<ul aria-label="قائمة المستخدمين">
{users.map((user) => (
<li key={user.id}>
<strong>{user.name}</strong> — {user.email}
</li>
))}
</ul>
);
}اختبره:
import { render, screen } from "@testing-library/react";
import { vi } from "vitest";
import { UserList } from "../UserList";
describe("UserList", () => {
it("يعرض حالة التحميل", () => {
global.fetch = vi.fn().mockImplementation(
() => new Promise(() => {}) // وعد لا يُحل أبداً
);
render(<UserList />);
expect(screen.getByText("جارٍ التحميل...")).toBeInTheDocument();
});
it("يعرض قائمة المستخدمين", async () => {
const mockUsers = [
{ id: 1, name: "أحمد", email: "ahmed@example.com" },
{ id: 2, name: "سارة", email: "sara@example.com" },
];
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: () => Promise.resolve(mockUsers),
});
render(<UserList />);
expect(await screen.findByText("أحمد")).toBeInTheDocument();
expect(screen.getByText("سارة")).toBeInTheDocument();
expect(screen.getByRole("list")).toHaveAttribute(
"aria-label",
"قائمة المستخدمين"
);
});
it("يعرض رسالة خطأ عند الفشل", async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: false,
status: 500,
});
render(<UserList />);
expect(
await screen.findByText("خطأ: خطأ في الخادم")
).toBeInTheDocument();
});
});نصيحة: استخدم findByText بدلاً من getByText للعناصر التي تظهر بشكل غير متزامن. findByText تنتظر تلقائياً حتى يكون العنصر موجوداً في الـ DOM.
الخطوة 10: اختبار مسارات API (Route Handlers)
يستخدم Next.js 15 الـ Route Handlers في مجلد app/api/. أنشئ src/app/api/users/route.ts:
import { NextRequest, NextResponse } from "next/server";
const users = [
{ id: 1, name: "أحمد", email: "ahmed@example.com" },
{ id: 2, name: "سارة", email: "sara@example.com" },
];
export async function GET() {
return NextResponse.json(users);
}
export async function POST(request: NextRequest) {
const body = await request.json();
if (!body.name || !body.email) {
return NextResponse.json(
{ error: "الاسم والبريد الإلكتروني مطلوبان" },
{ status: 400 }
);
}
const newUser = {
id: users.length + 1,
name: body.name,
email: body.email,
};
return NextResponse.json(newUser, { status: 201 });
}اختبر الـ Route Handlers مباشرة:
import { GET, POST } from "@/app/api/users/route";
import { NextRequest } from "next/server";
describe("API /api/users", () => {
describe("GET", () => {
it("يرجع قائمة المستخدمين", async () => {
const response = await GET();
const data = await response.json();
expect(response.status).toBe(200);
expect(data).toHaveLength(2);
expect(data[0]).toHaveProperty("name", "أحمد");
});
});
describe("POST", () => {
it("ينشئ مستخدماً جديداً", async () => {
const request = new NextRequest("http://localhost/api/users", {
method: "POST",
body: JSON.stringify({ name: "خالد", email: "khaled@example.com" }),
});
const response = await POST(request);
const data = await response.json();
expect(response.status).toBe(201);
expect(data).toMatchObject({
name: "خالد",
email: "khaled@example.com",
});
});
it("يرجع 400 إذا كانت البيانات ناقصة", async () => {
const request = new NextRequest("http://localhost/api/users", {
method: "POST",
body: JSON.stringify({ name: "خالد" }),
});
const response = await POST(request);
const data = await response.json();
expect(response.status).toBe(400);
expect(data.error).toBe("الاسم والبريد الإلكتروني مطلوبان");
});
});
});الخطوة 11: الاختبار مع Providers (Context، Theme، إلخ.)
معظم التطبيقات تستخدم providers في React. أنشئ أداة عرض مخصصة:
// src/test/test-utils.tsx
import { ReactElement } from "react";
import { render, RenderOptions } from "@testing-library/react";
function AllProviders({ children }: { children: React.ReactNode }) {
return <>{children}</>;
}
function customRender(
ui: ReactElement,
options?: Omit<RenderOptions, "wrapper">
) {
return render(ui, { wrapper: AllProviders, ...options });
}
export * from "@testing-library/react";
export { customRender as render };استخدمه في اختباراتك:
// بدلاً من:
import { render, screen } from "@testing-library/react";
// استخدم:
import { render, screen } from "@/test/test-utils";الخطوة 12: Snapshots والاختبارات البصرية
يدعم Vitest اختبارات snapshot للكشف عن التغييرات غير المتوقعة:
import { render } from "@testing-library/react";
import { Counter } from "../Counter";
it("يطابق الـ snapshot", () => {
const { container } = render(<Counter initialCount={5} />);
expect(container).toMatchSnapshot();
});
// أو مع inline snapshots (أكثر قابلية للقراءة)
it("يطابق الـ inline snapshot", () => {
const { container } = render(<Counter />);
expect(container.firstChild).toMatchInlineSnapshot(`
<div>
<p data-testid="count-display">العداد: 0</p>
<button>زيادة</button>
<button>إنقاص</button>
<button>إعادة تعيين</button>
</div>
`);
});تحذير: الـ snapshots مفيدة للكشف عن الانحدارات، لكن لا تستخدمها كبديل عن التأكيدات الصريحة. فضّل toHaveTextContent وtoBeInTheDocument للتحقق من السلوك.
الخطوة 13: تغطية الكود
شغّل الاختبارات مع التغطية:
npm run test:coverageينتج Vitest تقرير تغطية مفصل:
-----------------------|---------|----------|---------|---------|
File | % Stmts | % Branch | % Funcs | % Lines |
-----------------------|---------|----------|---------|---------|
All files | 92.3 | 85.7 | 100 | 92.3 |
components/Counter | 100 | 100 | 100 | 100 |
components/LoginForm | 95.2 | 83.3 | 100 | 95.2 |
hooks/useLocalStorage | 88.9 | 75.0 | 100 | 88.9 |
-----------------------|---------|----------|---------|---------|
إعداد حدود التغطية
أضف حدوداً دنيا في vitest.config.ts:
coverage: {
provider: "v8",
thresholds: {
statements: 80,
branches: 75,
functions: 80,
lines: 80,
},
},إذا انخفضت التغطية تحت هذه الحدود، ستفشل الاختبارات في CI.
الخطوة 14: التكامل مع CI/CD باستخدام GitHub Actions
أنشئ .github/workflows/test.yml:
name: Tests
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: تثبيت Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"
- name: تثبيت التبعيات
run: npm ci
- name: تشغيل الاختبارات
run: npm run test:run
- name: التحقق من التغطية
run: npm run test:coverageالخطوة 15: أفضل الممارسات والأنماط المتقدمة
1. اتبع فلسفة Testing Library
"كلما كانت اختباراتك تشبه الطريقة التي يُستخدم بها برنامجك، كلما منحتك ثقة أكبر." — Kent C. Dodds
// ❌ سيء: اختبار تفاصيل التنفيذ
expect(component.state.isOpen).toBe(true);
// ✅ جيد: اختبار السلوك المرئي
expect(screen.getByRole("dialog")).toBeVisible();2. فضّل الاستعلامات بالدور
// ❌ هش: يعتمد على النص الدقيق
screen.getByText("إرسال");
// ✅ متين: يعتمد على الدور المتاح
screen.getByRole("button", { name: /إرسال/i });3. استخدم userEvent بدلاً من fireEvent
// ❌ fireEvent منخفض المستوى
fireEvent.click(button);
// ✅ userEvent يحاكي السلوك الحقيقي للمستخدم
const user = userEvent.setup();
await user.click(button);4. نظّم باستخدام نمط AAA
it("يفلتر المستخدمين بالاسم", async () => {
// Arrange - الترتيب
const user = userEvent.setup();
render(<UserSearch users={mockUsers} />);
// Act - التنفيذ
await user.type(screen.getByRole("searchbox"), "أحمد");
// Assert - التأكيد
expect(screen.getByText("أحمد")).toBeInTheDocument();
expect(screen.queryByText("سارة")).not.toBeInTheDocument();
});5. تجنب الاختبارات غير المستقرة
// ❌ توقيت هش
await new Promise((resolve) => setTimeout(resolve, 1000));
expect(screen.getByText("تم التحميل")).toBeInTheDocument();
// ✅ انتظار مبني على التأكيدات
expect(await screen.findByText("تم التحميل")).toBeInTheDocument();6. نظّف الـ Mocks
afterEach(() => {
vi.restoreAllMocks();
});استكشاف الأخطاء
"ReferenceError: document is not defined"
تحقق من أن البيئة مضبوطة على jsdom في vitest.config.ts:
test: {
environment: "jsdom",
}"Cannot find module '@/...'"
تأكد من إعداد الأسماء المستعارة في vitest.config.ts:
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},الاختبارات بطيئة
- استخدم
--reporter=verboseلتحديد الاختبارات البطيئة - تجنب
setTimeoutفي الاختبارات - استخدم
vi.useFakeTimers()للمكونات ذات المؤقتات - شغّل الاختبارات بالتوازي (السلوك الافتراضي لـ Vitest)
الـ Mock لا يعمل
تأكد من أن vi.mock() يُستدعى على مستوى الوحدة (وليس داخل describe أو it):
// ✅ صحيح: على مستوى الوحدة
vi.mock("next/navigation", () => ({
useRouter: () => ({ push: vi.fn() }),
}));
// ❌ خاطئ: داخل كتلة اختبار
describe("MyComponent", () => {
vi.mock("next/navigation"); // متأخر جداً!
});الخطوات التالية
الآن بعد أن أصبحت اختبارات الوحدة جاهزة:
- استكشف اختبارات E2E مع Playwright لإكمال استراتيجية الاختبار — راجع دليل Playwright
- أضف اختبارات snapshot بصرية مع Chromatic أو Percy
- أعدّ اختبار الطفرات (mutation testing) مع Stryker للتحقق من جودة اختباراتك
- استكشف وضع Vitest Browser Mode للاختبار في متصفح حقيقي
الخلاصة
لديك الآن بيئة اختبار كاملة لتطبيق Next.js 15 الخاص بك:
- Vitest مُعدّ مع React Testing Library لاختبارات سريعة وموثوقة
- اختبارات للـ مكونات والـ hooks والـ نماذج ومسارات API
- إتقان الـ mocking للوحدات وfetch والـ providers
- تغطية الكود مقاسة بحدود قابلة للتعديل
- خط أنابيب CI/CD جاهز للإنتاج
الاختبارات ليست عبئاً — إنها شبكة أمانك. كل اختبار تكتبه هو مواصفة حية لتطبيقك، حارس ضد الانحدارات، وتوثيق لا يكذب أبداً. استثمر في اختباراتك اليوم، ومستقبلك سيشكرك.
ناقش مشروعك معنا
نحن هنا للمساعدة في احتياجات تطوير الويب الخاصة بك. حدد موعدًا لمناقشة مشروعك وكيف يمكننا مساعدتك.
دعنا نجد أفضل الحلول لاحتياجاتك.
مقالات ذات صلة

بناء وكيل ذكاء اصطناعي مستقل باستخدام Agentic RAG و Next.js
تعلم كيف تبني وكيل ذكاء اصطناعي يقرر بشكل مستقل متى وكيف يسترجع المعلومات من قواعد البيانات المتجهية. دليل عملي شامل باستخدام Vercel AI SDK و Next.js مع أمثلة قابلة للتنفيذ.

Better Auth مع Next.js 15: الدليل الشامل للمصادقة في 2026
تعلم كيفية تنفيذ نظام مصادقة متكامل في Next.js 15 باستخدام Better Auth. يغطي هذا الدليل تسجيل الدخول بالبريد الإلكتروني وOAuth والجلسات وحماية المسارات والتحكم بالأدوار.

بناء تطبيق متكامل باستخدام Drizzle ORM و Next.js 15: قاعدة بيانات آمنة الأنواع من الصفر إلى الإنتاج
تعلّم كيفية بناء تطبيق متكامل آمن الأنواع باستخدام Drizzle ORM مع Next.js 15. يغطي هذا الدليل العملي تصميم المخططات والترحيلات وServer Actions وعمليات CRUD والنشر مع PostgreSQL.