العربية

أتقن مكتبة اختبار React (RTL) مع هذا الدليل الكامل. تعلم كيفية كتابة اختبارات فعالة وقابلة للصيانة وموجهة للمستخدم لتطبيقات React الخاصة بك، مع التركيز على أفضل الممارسات والأمثلة الواقعية.

مكتبة اختبار React: دليل شامل

في عالم تطوير الويب سريع الخطى اليوم، يعد ضمان جودة وموثوقية تطبيقات React الخاصة بك أمرًا بالغ الأهمية. برزت مكتبة اختبار React (RTL) كحل شائع وفعال لكتابة الاختبارات التي تركز على منظور المستخدم. يقدم هذا الدليل نظرة عامة كاملة على RTL، ويغطي كل شيء بدءًا من المفاهيم الأساسية إلى التقنيات المتقدمة، مما يمكّنك من بناء تطبيقات React قوية وقابلة للصيانة.

لماذا تختار مكتبة اختبار React؟

تعتمد أساليب الاختبار التقليدية غالبًا على تفاصيل التنفيذ، مما يجعل الاختبارات هشة وعرضة للانهيار مع تغييرات طفيفة في الكود. من ناحية أخرى، تشجعك RTL على اختبار مكوناتك كما لو كان المستخدم يتفاعل معها، مع التركيز على ما يراه المستخدم ويختبره. يقدم هذا النهج العديد من المزايا الرئيسية:

إعداد بيئة الاختبار الخاصة بك

قبل أن تتمكن من البدء في استخدام RTL، تحتاج إلى إعداد بيئة الاختبار الخاصة بك. يتضمن هذا عادةً تثبيت التبعيات اللازمة وتكوين إطار عمل الاختبار الخاص بك.

المتطلبات الأساسية

التثبيت

قم بتثبيت الحزم التالية باستخدام 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

شرح الحزم:

الإعداد

أنشئ ملف `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();
});

الشرح:

لتشغيل الاختبار، نفذ الأمر التالي في الطرفية الخاصة بك:

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);
});

الشرح:

تقنيات الاختبار المتقدمة

بمجرد إتقان أساسيات 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);
});

الشرح:

موفرو السياق (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();
});

الشرح:

الاختبار باستخدام الموجه (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');
});

الشرح:

أفضل الممارسات لكتابة اختبارات فعالة

فيما يلي بعض أفضل الممارسات التي يجب اتباعها عند كتابة الاختبارات باستخدام RTL:

الخاتمة

مكتبة اختبار React هي أداة قوية لكتابة اختبارات فعالة وقابلة للصيانة وموجهة للمستخدم لتطبيقات React الخاصة بك. باتباع المبادئ والتقنيات الموضحة في هذا الدليل، يمكنك بناء تطبيقات قوية وموثوقة تلبي احتياجات المستخدمين. تذكر أن تركز على الاختبار من منظور المستخدم، وتجنب اختبار تفاصيل التنفيذ، واكتب اختبارات واضحة وموجزة. من خلال تبني RTL واعتماد أفضل الممارسات، يمكنك تحسين جودة وصيانة مشاريع React الخاصة بك بشكل كبير، بغض النظر عن موقعك أو المتطلبات المحددة لجمهورك العالمي.

مكتبة اختبار React: دليل شامل | MLOG