أتقن مكتبة اختبار React (RTL) مع هذا الدليل الكامل. تعلم كيفية كتابة اختبارات فعالة وقابلة للصيانة وموجهة للمستخدم لتطبيقات React الخاصة بك، مع التركيز على أفضل الممارسات والأمثلة الواقعية.
مكتبة اختبار React: دليل شامل
في عالم تطوير الويب سريع الخطى اليوم، يعد ضمان جودة وموثوقية تطبيقات React الخاصة بك أمرًا بالغ الأهمية. برزت مكتبة اختبار React (RTL) كحل شائع وفعال لكتابة الاختبارات التي تركز على منظور المستخدم. يقدم هذا الدليل نظرة عامة كاملة على RTL، ويغطي كل شيء بدءًا من المفاهيم الأساسية إلى التقنيات المتقدمة، مما يمكّنك من بناء تطبيقات React قوية وقابلة للصيانة.
لماذا تختار مكتبة اختبار React؟
تعتمد أساليب الاختبار التقليدية غالبًا على تفاصيل التنفيذ، مما يجعل الاختبارات هشة وعرضة للانهيار مع تغييرات طفيفة في الكود. من ناحية أخرى، تشجعك RTL على اختبار مكوناتك كما لو كان المستخدم يتفاعل معها، مع التركيز على ما يراه المستخدم ويختبره. يقدم هذا النهج العديد من المزايا الرئيسية:
- الاختبار المرتكز على المستخدم: تعزز RTL كتابة الاختبارات التي تعكس منظور المستخدم، مما يضمن أن تطبيقك يعمل كما هو متوقع من وجهة نظر المستخدم النهائي.
- تقليل هشاشة الاختبارات: من خلال تجنب اختبار تفاصيل التنفيذ، تكون اختبارات RTL أقل عرضة للانهيار عند إعادة بناء الكود الخاص بك، مما يؤدي إلى اختبارات أكثر قوة وقابلية للصيانة.
- تحسين تصميم الكود: تشجع RTL على كتابة مكونات يسهل الوصول إليها واستخدامها، مما يؤدي إلى تصميم كود أفضل بشكل عام.
- التركيز على إمكانية الوصول: تسهل RTL اختبار إمكانية الوصول لمكوناتك، مما يضمن أن تطبيقك قابل للاستخدام من قبل الجميع.
- تبسيط عملية الاختبار: توفر RTL واجهة برمجة تطبيقات (API) بسيطة وبديهية، مما يسهل كتابة الاختبارات وصيانتها.
إعداد بيئة الاختبار الخاصة بك
قبل أن تتمكن من البدء في استخدام RTL، تحتاج إلى إعداد بيئة الاختبار الخاصة بك. يتضمن هذا عادةً تثبيت التبعيات اللازمة وتكوين إطار عمل الاختبار الخاص بك.
المتطلبات الأساسية
- Node.js و npm (أو yarn): تأكد من تثبيت Node.js و npm (أو yarn) على نظامك. يمكنك تنزيلهما من موقع Node.js الرسمي.
- مشروع React: يجب أن يكون لديك مشروع React حالي أو إنشاء مشروع جديد باستخدام Create React App أو أداة مشابهة.
التثبيت
قم بتثبيت الحزم التالية باستخدام npm أو yarn:
npm install --save-dev @testing-library/react @testing-library/jest-dom jest babel-jest @babel/preset-env @babel/preset-react
أو باستخدام yarn:
yarn add --dev @testing-library/react @testing-library/jest-dom jest babel-jest @babel/preset-env @babel/preset-react
شرح الحزم:
- @testing-library/react: المكتبة الأساسية لاختبار مكونات React.
- @testing-library/jest-dom: توفر مُطابقات Jest مخصصة للتأكيد على عقد DOM.
- Jest: إطار عمل اختبار JavaScript شائع.
- babel-jest: محول Jest يستخدم Babel لترجمة الكود الخاص بك.
- @babel/preset-env: إعداد مسبق لـ Babel يحدد ملحقات وإعدادات Babel المسبقة اللازمة لدعم البيئات المستهدفة.
- @babel/preset-react: إعداد مسبق لـ Babel مخصص لـ React.
الإعداد
أنشئ ملف `babel.config.js` في جذر مشروعك بالمحتوى التالي:
module.exports = {
presets: ['@babel/preset-env', '@babel/preset-react'],
};
حدّث ملف `package.json` الخاص بك لتضمين نص برمجي للاختبار:
{
"scripts": {
"test": "jest"
}
}
أنشئ ملف `jest.config.js` في جذر مشروعك لتكوين Jest. قد يبدو التكوين الأدنى كما يلي:
module.exports = {
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['/src/setupTests.js'],
};
أنشئ ملف `src/setupTests.js` بالمحتوى التالي. هذا يضمن أن مُطابقات Jest DOM متاحة في جميع اختباراتك:
import '@testing-library/jest-dom/extend-expect';
كتابة أول اختبار لك
لنبدأ بمثال بسيط. افترض أن لديك مكون React يعرض رسالة ترحيب:
// src/components/Greeting.js
import React from 'react';
function Greeting({ name }) {
return <h1>Hello, {name}!</h1>;
}
export default Greeting;
الآن، لنكتب اختبارًا لهذا المكون:
// src/components/Greeting.test.js
import React from 'react';
import { render, screen } from '@testing-library/react';
import Greeting from './Greeting';
test('renders a greeting message', () => {
render(<Greeting name="World" />);
const greetingElement = screen.getByText(/Hello, World!/i);
expect(greetingElement).toBeInTheDocument();
});
الشرح:
- `render`: تقوم هذه الدالة بعرض المكون في DOM.
- `screen`: يوفر هذا الكائن طرقًا للاستعلام عن DOM.
- `getByText`: تجد هذه الطريقة عنصرًا حسب محتواه النصي. علامة `/i` تجعل البحث غير حساس لحالة الأحرف.
- `expect`: تُستخدم هذه الدالة لعمل تأكيدات حول سلوك المكون.
- `toBeInTheDocument`: يؤكد هذا المُطابق أن العنصر موجود في DOM.
لتشغيل الاختبار، نفذ الأمر التالي في الطرفية الخاصة بك:
npm test
إذا تم تكوين كل شيء بشكل صحيح، فيجب أن ينجح الاختبار.
استعلامات RTL الشائعة
توفر RTL طرق استعلام متنوعة للعثور على العناصر في DOM. تم تصميم هذه الاستعلامات لتقليد كيفية تفاعل المستخدمين مع تطبيقك.
`getByRole`
يجد هذا الاستعلام عنصرًا حسب دور ARIA الخاص به. من الممارسات الجيدة استخدام `getByRole` كلما أمكن ذلك، لأنه يعزز إمكانية الوصول ويضمن أن اختباراتك مرنة للتغييرات في بنية DOM الأساسية.
<button role="button">Click me</button>
const buttonElement = screen.getByRole('button');
expect(buttonElement).toBeInTheDocument();
`getByLabelText`
يجد هذا الاستعلام عنصرًا من خلال نص التسمية المرتبطة به. إنه مفيد لاختبار عناصر النموذج.
<label htmlFor="name">Name:</label>
<input type="text" id="name" />
const nameInputElement = screen.getByLabelText('Name:');
expect(nameInputElement).toBeInTheDocument();
`getByPlaceholderText`
يجد هذا الاستعلام عنصرًا من خلال نصه النائب.
<input type="text" placeholder="Enter your email" />
const emailInputElement = screen.getByPlaceholderText('Enter your email');
expect(emailInputElement).toBeInTheDocument();
`getByAltText`
يجد هذا الاستعلام عنصر صورة من خلال نصه البديل (alt text). من المهم توفير نص بديل ذي معنى لجميع الصور لضمان إمكانية الوصول.
<img src="logo.png" alt="Company Logo" />
const logoImageElement = screen.getByAltText('Company Logo');
expect(logoImageElement).toBeInTheDocument();
`getByTitle`
يجد هذا الاستعلام عنصرًا من خلال سمة العنوان (title) الخاصة به.
<span title="Close">X</span>
const closeElement = screen.getByTitle('Close');
expect(closeElement).toBeInTheDocument();
`getByDisplayValue`
يجد هذا الاستعلام عنصرًا من خلال قيمة العرض الخاصة به. هذا مفيد لاختبار مدخلات النموذج ذات القيم المعبأة مسبقًا.
<input type="text" value="Initial Value" />
const inputElement = screen.getByDisplayValue('Initial Value');
expect(inputElement).toBeInTheDocument();
استعلامات `getAllBy*`
بالإضافة إلى استعلامات `getBy*`، توفر RTL أيضًا استعلامات `getAllBy*`، والتي تُرجع مصفوفة من العناصر المطابقة. هذه مفيدة عندما تحتاج إلى التأكيد على وجود عناصر متعددة بنفس الخصائص في DOM.
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
const listItems = screen.getAllByRole('listitem');
expect(listItems).toHaveLength(3);
استعلامات `queryBy*`
تشبه استعلامات `queryBy*` استعلامات `getBy*`، لكنها تُرجع `null` إذا لم يتم العثور على عنصر مطابق، بدلاً من إلقاء خطأ. هذا مفيد عندما تريد التأكيد على أن عنصرًا ما *غير* موجود في DOM.
const missingElement = screen.queryByText('Non-existent text');
expect(missingElement).toBeNull();
استعلامات `findBy*`
تعتبر استعلامات `findBy*` إصدارات غير متزامنة من استعلامات `getBy*`. إنها تُرجع Promise يتم حله عند العثور على العنصر المطابق. هذه مفيدة لاختبار العمليات غير المتزامنة، مثل جلب البيانات من واجهة برمجة تطبيقات (API).
// Simulating an asynchronous data fetch
const fetchData = () => new Promise(resolve => {
setTimeout(() => resolve('Data Loaded!'), 1000);
});
function MyComponent() {
const [data, setData] = React.useState(null);
React.useEffect(() => {
fetchData().then(setData);
}, []);
return <div>{data}</div>;
}
test('loads data asynchronously', async () => {
render(<MyComponent />);
const dataElement = await screen.findByText('Data Loaded!');
expect(dataElement).toBeInTheDocument();
});
محاكاة تفاعلات المستخدم
توفر RTL واجهات برمجة التطبيقات `fireEvent` و `userEvent` لمحاكاة تفاعلات المستخدم، مثل النقر على الأزرار والكتابة في حقول الإدخال وتقديم النماذج.
`fireEvent`
`fireEvent` يسمح لك بإطلاق أحداث DOM برمجيًا. إنها واجهة برمجة تطبيقات منخفضة المستوى تمنحك تحكمًا دقيقًا في الأحداث التي يتم إطلاقها.
<button onClick={() => alert('Button clicked!')}>Click me</button>
import { fireEvent } from '@testing-library/react';
test('simulates a button click', () => {
const alertMock = jest.spyOn(window, 'alert').mockImplementation(() => {});
render(<button onClick={() => alert('Button clicked!')}>Click me</button>);
const buttonElement = screen.getByRole('button');
fireEvent.click(buttonElement);
expect(alertMock).toHaveBeenCalledTimes(1);
alertMock.mockRestore();
});
`userEvent`
`userEvent` هي واجهة برمجة تطبيقات عالية المستوى تحاكي تفاعلات المستخدم بشكل أكثر واقعية. إنها تتعامل مع تفاصيل مثل إدارة التركيز وترتيب الأحداث، مما يجعل اختباراتك أكثر قوة وأقل هشاشة.
<input type="text" onChange={e => console.log(e.target.value)} />
import userEvent from '@testing-library/user-event';
test('simulates typing in an input field', () => {
const inputElement = screen.getByRole('textbox');
userEvent.type(inputElement, 'Hello, world!');
expect(inputElement).toHaveValue('Hello, world!');
});
اختبار الكود غير المتزامن
تتضمن العديد من تطبيقات React عمليات غير متزامنة، مثل جلب البيانات من واجهة برمجة تطبيقات. توفر RTL العديد من الأدوات لاختبار الكود غير المتزامن.
`waitFor`
تسمح لك `waitFor` بالانتظار حتى يتحقق شرط ما قبل إجراء تأكيد. إنها مفيدة لاختبار العمليات غير المتزامنة التي تستغرق بعض الوقت لإكمالها.
function MyComponent() {
const [data, setData] = React.useState(null);
React.useEffect(() => {
setTimeout(() => {
setData('Data loaded!');
}, 1000);
}, []);
return <div>{data}</div>;
}
import { waitFor } from '@testing-library/react';
test('waits for data to load', async () => {
render(<MyComponent />);
await waitFor(() => screen.getByText('Data loaded!'));
const dataElement = screen.getByText('Data loaded!');
expect(dataElement).toBeInTheDocument();
});
استعلامات `findBy*`
كما ذكرنا سابقًا، فإن استعلامات `findBy*` غير متزامنة وتُرجع Promise يتم حله عند العثور على العنصر المطابق. هذه مفيدة لاختبار العمليات غير المتزامنة التي تؤدي إلى تغييرات في DOM.
اختبار الخطافات (Hooks)
خطافات React هي دوال قابلة لإعادة الاستخدام تغلف المنطق الذي يحتوي على حالة. توفر RTL الأداة المساعدة `renderHook` من `@testing-library/react-hooks` (التي تم إهمالها لصالح `@testing-library/react` مباشرة اعتبارًا من الإصدار 17) لاختبار الخطافات المخصصة بشكل معزول.
// src/hooks/useCounter.js
import { useState } from 'react';
function useCounter(initialValue = 0) {
const [count, setCount] = useState(initialValue);
const increment = () => {
setCount(prevCount => prevCount + 1);
};
const decrement = () => {
setCount(prevCount => prevCount - 1);
};
return { count, increment, decrement };
}
export default useCounter;
// src/hooks/useCounter.test.js
import { renderHook, act } from '@testing-library/react';
import useCounter from './useCounter';
test('increments the counter', () => {
const { result } = renderHook(() => useCounter());
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
الشرح:
- `renderHook`: تقوم هذه الدالة بعرض الخطاف وتُرجع كائنًا يحتوي على نتيجة الخطاف.
- `act`: تُستخدم هذه الدالة لتغليف أي كود يسبب تحديثات للحالة. هذا يضمن أن React يمكنه تجميع التحديثات ومعالجتها بشكل صحيح.
تقنيات الاختبار المتقدمة
بمجرد إتقان أساسيات RTL، يمكنك استكشاف تقنيات اختبار أكثر تقدمًا لتحسين جودة اختباراتك وقابليتها للصيانة.
محاكاة الوحدات (Mocking Modules)
في بعض الأحيان، قد تحتاج إلى محاكاة الوحدات أو التبعيات الخارجية لعزل مكوناتك والتحكم في سلوكها أثناء الاختبار. توفر Jest واجهة برمجة تطبيقات قوية للمحاكاة لهذا الغرض.
// src/api/dataService.js
export const fetchData = async () => {
const response = await fetch('/api/data');
const data = await response.json();
return data;
};
// src/components/MyComponent.js
import React, { useState, useEffect } from 'react';
import { fetchData } from '../api/dataService';
function MyComponent() {
const [data, setData] = useState(null);
useEffect(() => {
fetchData().then(setData);
}, []);
return <div>{data}</div>;
}
// src/components/MyComponent.test.js
import { render, screen, waitFor } from '@testing-library/react';
import MyComponent from './MyComponent';
import * as dataService from '../api/dataService';
jest.mock('../api/dataService');
test('fetches data from the API', async () => {
dataService.fetchData.mockResolvedValue({ message: 'Mocked data!' });
render(<MyComponent />);
await waitFor(() => screen.getByText('Mocked data!'));
expect(screen.getByText('Mocked data!')).toBeInTheDocument();
expect(dataService.fetchData).toHaveBeenCalledTimes(1);
});
الشرح:
- `jest.mock('../api/dataService')`: يقوم هذا السطر بمحاكاة وحدة `dataService`.
- `dataService.fetchData.mockResolvedValue({ message: 'Mocked data!' })`: يقوم هذا السطر بتكوين دالة `fetchData` المحاكاة لإرجاع Promise يتم حله بالبيانات المحددة.
- `expect(dataService.fetchData).toHaveBeenCalledTimes(1)`: يؤكد هذا السطر أنه تم استدعاء دالة `fetchData` المحاكاة مرة واحدة.
موفرو السياق (Context Providers)
إذا كان مكونك يعتمد على موفر سياق (Context Provider)، فستحتاج إلى تغليف مكونك في الموفر أثناء الاختبار. هذا يضمن أن المكون لديه حق الوصول إلى قيم السياق.
// src/contexts/ThemeContext.js
import React, { createContext, useState } from 'react';
export const ThemeContext = createContext();
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(prevTheme => (prevTheme === 'light' ? 'dark' : 'light'));
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
// src/components/MyComponent.js
import React, { useContext } from 'react';
import { ThemeContext } from '../contexts/ThemeContext';
function MyComponent() {
const { theme, toggleTheme } = useContext(ThemeContext);
return (
<div style={{ backgroundColor: theme === 'light' ? '#fff' : '#000', color: theme === 'light' ? '#000' : '#fff' }}>
<p>Current theme: {theme}</p>
<button onClick={toggleTheme}>Toggle Theme</button>
</div>
);
}
// src/components/MyComponent.test.js
import { render, screen, fireEvent } from '@testing-library/react';
import MyComponent from './MyComponent';
import { ThemeProvider } from '../contexts/ThemeContext';
test('toggles the theme', () => {
render(
<ThemeProvider>
<MyComponent />
</ThemeProvider>
);
const themeParagraph = screen.getByText(/Current theme: light/i);
const toggleButton = screen.getByRole('button', { name: /Toggle Theme/i });
expect(themeParagraph).toBeInTheDocument();
fireEvent.click(toggleButton);
expect(screen.getByText(/Current theme: dark/i)).toBeInTheDocument();
});
الشرح:
- نقوم بتغليف `MyComponent` في `ThemeProvider` لتوفير السياق اللازم أثناء الاختبار.
الاختبار باستخدام الموجه (Router)
عند اختبار المكونات التي تستخدم React Router، ستحتاج إلى توفير سياق موجه وهمي. يمكنك تحقيق ذلك باستخدام مكون `MemoryRouter` من `react-router-dom`.
// src/components/MyComponent.js
import React from 'react';
import { Link } from 'react-router-dom';
function MyComponent() {
return (
<div>
<Link to="/about">Go to About Page</Link>
</div>
);
}
// src/components/MyComponent.test.js
import { render, screen } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import MyComponent from './MyComponent';
test('renders a link to the about page', () => {
render(
<MemoryRouter>
<MyComponent />
</MemoryRouter>
);
const linkElement = screen.getByRole('link', { name: /Go to About Page/i });
expect(linkElement).toBeInTheDocument();
expect(linkElement).toHaveAttribute('href', '/about');
});
الشرح:
- نقوم بتغليف `MyComponent` في `MemoryRouter` لتوفير سياق موجه وهمي.
- نتأكد من أن عنصر الرابط له السمة `href` الصحيحة.
أفضل الممارسات لكتابة اختبارات فعالة
فيما يلي بعض أفضل الممارسات التي يجب اتباعها عند كتابة الاختبارات باستخدام RTL:
- التركيز على تفاعلات المستخدم: اكتب اختبارات تحاكي كيفية تفاعل المستخدمين مع تطبيقك.
- تجنب اختبار تفاصيل التنفيذ: لا تختبر الأعمال الداخلية لمكوناتك. بدلاً من ذلك، ركز على السلوك الملحوظ.
- كتابة اختبارات واضحة وموجزة: اجعل اختباراتك سهلة الفهم والصيانة.
- استخدام أسماء اختبار ذات معنى: اختر أسماء اختبار تصف بدقة السلوك الذي يتم اختباره.
- الحفاظ على عزل الاختبارات: تجنب التبعيات بين الاختبارات. يجب أن يكون كل اختبار مستقلاً وقائمًا بذاته.
- اختبار الحالات القصوى: لا تختبر المسار السعيد فقط. تأكد من اختبار الحالات القصوى وحالات الخطأ أيضًا.
- كتابة الاختبارات قبل كتابة الكود: فكر في استخدام التطوير القائم على الاختبار (TDD) لكتابة الاختبارات قبل كتابة الكود الخاص بك.
- اتبع نمط "AAA": ترتيب، تنفيذ، تأكيد (Arrange, Act, Assert). يساعد هذا النمط في تنظيم اختباراتك وجعلها أكثر قابلية للقراءة.
- حافظ على سرعة اختباراتك: يمكن أن تثبط الاختبارات البطيئة المطورين عن تشغيلها بشكل متكرر. قم بتحسين اختباراتك من حيث السرعة عن طريق محاكاة طلبات الشبكة وتقليل كمية التلاعب بـ DOM.
- استخدم رسائل خطأ وصفية: عندما تفشل التأكيدات، يجب أن توفر رسائل الخطأ معلومات كافية لتحديد سبب الفشل بسرعة.
الخاتمة
مكتبة اختبار React هي أداة قوية لكتابة اختبارات فعالة وقابلة للصيانة وموجهة للمستخدم لتطبيقات React الخاصة بك. باتباع المبادئ والتقنيات الموضحة في هذا الدليل، يمكنك بناء تطبيقات قوية وموثوقة تلبي احتياجات المستخدمين. تذكر أن تركز على الاختبار من منظور المستخدم، وتجنب اختبار تفاصيل التنفيذ، واكتب اختبارات واضحة وموجزة. من خلال تبني RTL واعتماد أفضل الممارسات، يمكنك تحسين جودة وصيانة مشاريع React الخاصة بك بشكل كبير، بغض النظر عن موقعك أو المتطلبات المحددة لجمهورك العالمي.