فارسی

با این راهنمای کامل، بر کتابخانه تست ری‌اکت (RTL) مسلط شوید. یاد بگیرید چگونه تست‌های مؤثر، قابل نگهداری و کاربرمحور برای برنامه‌های ری‌اکت خود بنویسید، با تمرکز بر بهترین شیوه‌ها و مثال‌های دنیای واقعی.

کتابخانه تست ری‌اکت: راهنمای جامع

در چشم‌انداز پرشتاب توسعه وب امروزی، تضمین کیفیت و قابلیت اطمینان برنامه‌های ری‌اکت شما از اهمیت بالایی برخوردار است. کتابخانه تست ری‌اکت (RTL) به عنوان یک راه‌حل محبوب و مؤثر برای نوشتن تست‌هایی که بر دیدگاه کاربر تمرکز دارند، ظهور کرده است. این راهنما یک نمای کلی از RTL ارائه می‌دهد که همه چیز را از مفاهیم بنیادی تا تکنیک‌های پیشرفته پوشش می‌دهد و شما را برای ساخت برنامه‌های ری‌اکت قوی و قابل نگهداری توانمند می‌سازد.

چرا کتابخانه تست ری‌اکت را انتخاب کنیم؟

رویکردهای سنتی تست‌نویسی اغلب به جزئیات پیاده‌سازی متکی هستند، که این امر تست‌ها را شکننده و مستعد شکستن با تغییرات جزئی کد می‌کند. در مقابل، 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';

نوشتن اولین تست شما

بیایید با یک مثال ساده شروع کنیم. فرض کنید یک کامپوننت ری‌اکت دارید که یک پیام خوش‌آمدگویی نمایش می‌دهد:

// 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) مرتبط با آن پیدا می‌کند. این برای تست عناصر فرم مفید است.

<label htmlFor="name">Name:</label>
<input type="text" id="name" />
const nameInputElement = screen.getByLabelText('Name:');
expect(nameInputElement).toBeInTheDocument();

`getByPlaceholderText`

این کوئری یک عنصر را بر اساس متن placeholder آن پیدا می‌کند.

<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`

این کوئری یک عنصر را بر اساس مقدار نمایش داده شده (display value) آن پیدا می‌کند. این برای تست ورودی‌های فرم با مقادیر از پیش پر شده مفید است.

<input type="text" value="Initial Value" />
const inputElement = screen.getByDisplayValue('Initial Value');
expect(inputElement).toBeInTheDocument();

`getAllBy*` Queries

علاوه بر کوئری‌های `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*` Queries

کوئری‌های `queryBy*` شبیه به کوئری‌های `getBy*` هستند، اما اگر هیچ عنصر منطبقی پیدا نشود، به جای پرتاب خطا، `null` برمی‌گردانند. این زمانی مفید است که می‌خواهید ارزیابی کنید که یک عنصر در DOM حضور *ندارد*.

const missingElement = screen.queryByText('Non-existent text');
expect(missingElement).toBeNull();

`findBy*` Queries

کوئری‌های `findBy*` نسخه‌های ناهمزمان (asynchronous) کوئری‌های `getBy*` هستند. آن‌ها یک Promise برمی‌گردانند که وقتی عنصر منطبق پیدا شود، resolve می‌شود. این‌ها برای تست عملیات ناهمزمان، مانند واکشی داده از یک API، مفید هستند.

// شبیه‌سازی یک واکشی داده ناهمزمان
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 را به صورت برنامه‌نویسی فعال کنید. این یک API سطح پایین‌تر است که به شما کنترل دقیقی بر رویدادهایی که فعال می‌شوند می‌دهد.

<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` یک API سطح بالاتر است که تعاملات کاربر را به طور واقعی‌تری شبیه‌سازی می‌کند. این API جزئیاتی مانند مدیریت فوکوس و ترتیب رویدادها را کنترل می‌کند، که تست‌های شما را قوی‌تر و کمتر شکننده می‌کند.

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

تست کد ناهمزمان

بسیاری از برنامه‌های ری‌اکت شامل عملیات ناهمزمان، مانند واکشی داده از یک API، هستند. 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*` Queries

همانطور که قبلاً ذکر شد، کوئری‌های `findBy*` ناهمزمان هستند و یک Promise برمی‌گردانند که وقتی عنصر منطبق پیدا شود، resolve می‌شود. این‌ها برای تست عملیات ناهمزمانی که منجر به تغییراتی در DOM می‌شوند، مفید هستند.

تست هوک‌ها

هوک‌های ری‌اکت توابع قابل استفاده مجددی هستند که منطق حالت‌دار (stateful) را کپسوله می‌کنند. RTL ابزار `renderHook` را از `@testing-library/react-hooks` (که به نفع `@testing-library/react` از نسخه ۱۷ منسوخ شده است) برای تست هوک‌های سفارشی به صورت مجزا فراهم می‌کند.

// 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)

گاهی اوقات، ممکن است نیاز داشته باشید ماژول‌ها یا وابستگی‌های خارجی را ماک (mock) کنید تا کامپوننت‌های خود را ایزوله کرده و رفتار آن‌ها را در حین تست کنترل کنید. Jest یک API ماکینگ قدرتمند برای این منظور فراهم می‌کند.

// 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 آورده شده است:

نتیجه‌گیری

کتابخانه تست ری‌اکت ابزاری قدرتمند برای نوشتن تست‌های مؤثر، قابل نگهداری و کاربرمحور برای برنامه‌های ری‌اکت شما است. با پیروی از اصول و تکنیک‌های ذکر شده در این راهنما، می‌توانید برنامه‌های قوی و قابل اعتمادی بسازید که نیازهای کاربران شما را برآورده کنند. به یاد داشته باشید که بر تست از دیدگاه کاربر تمرکز کنید، از تست جزئیات پیاده‌سازی اجتناب کنید و تست‌های واضح و مختصر بنویسید. با پذیرش RTL و اتخاذ بهترین شیوه‌ها، می‌توانید کیفیت و قابلیت نگهداری پروژه‌های ری‌اکت خود را به طور قابل توجهی بهبود بخشید، صرف نظر از موقعیت مکانی شما یا الزامات خاص مخاطبان جهانی شما.