با این راهنمای کامل، بر کتابخانه تست ریاکت (RTL) مسلط شوید. یاد بگیرید چگونه تستهای مؤثر، قابل نگهداری و کاربرمحور برای برنامههای ریاکت خود بنویسید، با تمرکز بر بهترین شیوهها و مثالهای دنیای واقعی.
کتابخانه تست ریاکت: راهنمای جامع
در چشمانداز پرشتاب توسعه وب امروزی، تضمین کیفیت و قابلیت اطمینان برنامههای ریاکت شما از اهمیت بالایی برخوردار است. کتابخانه تست ریاکت (RTL) به عنوان یک راهحل محبوب و مؤثر برای نوشتن تستهایی که بر دیدگاه کاربر تمرکز دارند، ظهور کرده است. این راهنما یک نمای کلی از RTL ارائه میدهد که همه چیز را از مفاهیم بنیادی تا تکنیکهای پیشرفته پوشش میدهد و شما را برای ساخت برنامههای ریاکت قوی و قابل نگهداری توانمند میسازد.
چرا کتابخانه تست ریاکت را انتخاب کنیم؟
رویکردهای سنتی تستنویسی اغلب به جزئیات پیادهسازی متکی هستند، که این امر تستها را شکننده و مستعد شکستن با تغییرات جزئی کد میکند. در مقابل، RTL شما را تشویق میکند تا کامپوننتهای خود را همانطور که یک کاربر با آنها تعامل میکند، تست کنید و بر آنچه کاربر میبیند و تجربه میکند تمرکز کنید. این رویکرد چندین مزیت کلیدی ارائه میدهد:
- تست کاربرمحور: RTL نوشتن تستهایی را ترویج میدهد که دیدگاه کاربر را منعکس میکنند و تضمین میکند که برنامه شما از دید کاربر نهایی همانطور که انتظار میرود عمل میکند.
- کاهش شکنندگی تست: با اجتناب از تست جزئیات پیادهسازی، تستهای RTL کمتر احتمال دارد با بازنویسی (refactor) کد شما شکسته شوند، که منجر به تستهای قابل نگهداریتر و قویتر میشود.
- بهبود طراحی کد: RTL شما را تشویق میکند تا کامپوننتهایی بنویسید که در دسترس و آسان برای استفاده باشند، که به طراحی کلی کد بهتر منجر میشود.
- تمرکز بر دسترسیپذیری (Accessibility): RTL تست دسترسیپذیری کامپوننتهای شما را آسانتر میکند و تضمین میکند که برنامه شما برای همه قابل استفاده است.
- فرآیند تست سادهشده: RTL یک API ساده و شهودی ارائه میدهد که نوشتن و نگهداری تستها را آسانتر میکند.
راهاندازی محیط تست
قبل از اینکه بتوانید از RTL استفاده کنید، باید محیط تست خود را راهاندازی کنید. این کار معمولاً شامل نصب وابستگیهای لازم و پیکربندی فریمورک تست شما است.
پیشنیازها
- Node.js و npm (یا yarn): اطمینان حاصل کنید که Node.js و npm (یا yarn) روی سیستم شما نصب شدهاند. میتوانید آنها را از وبسایت رسمی Node.js دانلود کنید.
- پروژه ریاکت: شما باید یک پروژه ریاکت موجود داشته باشید یا یک پروژه جدید با استفاده از 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: کتابخانه اصلی برای تست کامپوننتهای ریاکت.
- @testing-library/jest-dom: تطبیقدهندههای (matchers) سفارشی Jest برای ارزیابی گرههای DOM را فراهم میکند.
- Jest: یک فریمورک محبوب تست جاوااسکریپت.
- babel-jest: یک ترانسفورمر Jest که از Babel برای کامپایل کد شما استفاده میکند.
- @babel/preset-env: یک پریست Babel که پلاگینها و پریستهای Babel مورد نیاز برای پشتیبانی از محیطهای هدف شما را تعیین میکند.
- @babel/preset-react: یک پریست Babel برای ریاکت.
پیکربندی
یک فایل `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();
});
توضیح:
- `render`: این تابع کامپوننت را در DOM رندر میکند.
- `screen`: این شیء متدهایی برای کوئری گرفتن از DOM فراهم میکند.
- `getByText`: این متد یک عنصر را بر اساس محتوای متنی آن پیدا میکند. پرچم `/i` جستجو را غیر حساس به حروف بزرگ و کوچک میکند.
- `expect`: این تابع برای انجام ارزیابی (assertion) در مورد رفتار کامپوننت استفاده میشود.
- `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) مرتبط با آن پیدا میکند. این برای تست عناصر فرم مفید است.
<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);
});
توضیح:
- `renderHook`: این تابع هوک را رندر میکند و یک شیء حاوی نتیجه هوک را برمیگرداند.
- `act`: این تابع برای پیچیدن هر کدی که باعث بهروزرسانی حالت میشود، استفاده میشود. این تضمین میکند که ریاکت میتواند بهروزرسانیها را به درستی دستهبندی و پردازش کند.
تکنیکهای تست پیشرفته
هنگامی که بر اصول اولیه 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);
});
توضیح:
- `jest.mock('../api/dataService')`: این خط ماژول `dataService` را ماک میکند.
- `dataService.fetchData.mockResolvedValue({ message: 'Mocked data!' })`: این خط تابع ماک شده `fetchData` را طوری پیکربندی میکند که یک Promise برگرداند که با دادههای مشخص شده resolve میشود.
- `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 آورده شده است:
- بر تعاملات کاربر تمرکز کنید: تستهایی بنویسید که نحوه تعامل کاربران با برنامه شما را شبیهسازی میکنند.
- از تست جزئیات پیادهسازی اجتناب کنید: عملکرد داخلی کامپوننتهای خود را تست نکنید. به جای آن، بر رفتار قابل مشاهده تمرکز کنید.
- تستهای واضح و مختصر بنویسید: تستهای خود را برای درک و نگهداری آسان کنید.
- از نامهای تست معنادار استفاده کنید: نامهایی برای تست انتخاب کنید که به طور دقیق رفتار مورد آزمایش را توصیف میکنند.
- تستها را ایزوله نگه دارید: از وابستگی بین تستها خودداری کنید. هر تست باید مستقل و خودکفا باشد.
- موارد مرزی (Edge Cases) را تست کنید: فقط مسیر خوشبینانه را تست نکنید. اطمینان حاصل کنید که موارد مرزی و شرایط خطا را نیز تست میکنید.
- قبل از کدنویسی تست بنویسید: استفاده از توسعه مبتنی بر تست (TDD) را برای نوشتن تست قبل از نوشتن کد خود در نظر بگیرید.
- از الگوی "AAA" پیروی کنید: Arrange, Act, Assert (آمادهسازی، عمل، ارزیابی). این الگو به ساختارمند کردن تستهای شما و خواناتر کردن آنها کمک میکند.
- تستهای خود را سریع نگه دارید: تستهای کند میتوانند توسعهدهندگان را از اجرای مکرر آنها دلسرد کنند. تستهای خود را با ماک کردن درخواستهای شبکه و به حداقل رساندن میزان دستکاری DOM برای سرعت بهینه کنید.
- از پیامهای خطای توصیفی استفاده کنید: وقتی ارزیابیها با شکست مواجه میشوند، پیامهای خطا باید اطلاعات کافی برای شناسایی سریع علت شکست را ارائه دهند.
نتیجهگیری
کتابخانه تست ریاکت ابزاری قدرتمند برای نوشتن تستهای مؤثر، قابل نگهداری و کاربرمحور برای برنامههای ریاکت شما است. با پیروی از اصول و تکنیکهای ذکر شده در این راهنما، میتوانید برنامههای قوی و قابل اعتمادی بسازید که نیازهای کاربران شما را برآورده کنند. به یاد داشته باشید که بر تست از دیدگاه کاربر تمرکز کنید، از تست جزئیات پیادهسازی اجتناب کنید و تستهای واضح و مختصر بنویسید. با پذیرش RTL و اتخاذ بهترین شیوهها، میتوانید کیفیت و قابلیت نگهداری پروژههای ریاکت خود را به طور قابل توجهی بهبود بخشید، صرف نظر از موقعیت مکانی شما یا الزامات خاص مخاطبان جهانی شما.