بر تست کامپوننت React با کتابخانه تست React مسلط شوید. بهترین شیوهها را برای نوشتن تستهای قابل نگهداری و مؤثر که بر رفتار کاربر و دسترسیپذیری تمرکز دارند، بیاموزید.
کتابخانه تست React: بهترین شیوهها برای تست کامپوننت برای تیمهای جهانی
در دنیای همیشه در حال تحول توسعه وب، اطمینان از قابلیت اطمینان و کیفیت برنامههای React شما بسیار مهم است. این امر به ویژه برای تیمهای جهانی که روی پروژههایی با پایگاههای کاربری متنوع و الزامات دسترسیپذیری کار میکنند، صادق است. کتابخانه تست React (RTL) یک رویکرد قدرتمند و کاربر-محور برای تست کامپوننت فراهم میکند. برخلاف روشهای تست سنتی که بر جزئیات پیادهسازی تمرکز دارند، RTL شما را تشویق میکند تا کامپوننتهای خود را همانطور که یک کاربر با آنها تعامل میکند، تست کنید، که منجر به تستهای قویتر و قابل نگهداریتر میشود. این راهنمای جامع به بررسی بهترین شیوهها برای استفاده از RTL در پروژههای React شما، با تمرکز بر ساخت برنامههای مناسب برای مخاطبان جهانی، میپردازد.
چرا کتابخانه تست React؟
قبل از پرداختن به بهترین شیوهها، درک اینکه چرا RTL از سایر کتابخانههای تست متمایز است، بسیار مهم است. در اینجا برخی از مزایای کلیدی آن آورده شده است:
- رویکرد کاربر-محور: RTL تست کامپوننتها را از دیدگاه کاربر در اولویت قرار میدهد. شما با استفاده از همان روشهایی که یک کاربر استفاده میکند (مانند کلیک کردن روی دکمهها، تایپ کردن در فیلدهای ورودی) با کامپوننت تعامل میکنید، که تجربه تست واقعیتر و قابل اعتمادتری را تضمین میکند.
- متمرکز بر دسترسیپذیری: RTL با تشویق شما به تست کامپوننتها به روشی که کاربران دارای معلولیت را در نظر میگیرد، نوشتن کامپوننتهای قابل دسترس را ترویج میکند. این با استانداردهای جهانی دسترسیپذیری مانند WCAG همسو است.
- کاهش نگهداری: با اجتناب از تست جزئیات پیادهسازی (مانند state داخلی، فراخوانی توابع خاص)، تستهای RTL کمتر احتمال دارد هنگام بازنویسی کد شما با شکست مواجه شوند. این منجر به تستهای قابل نگهداری و مقاومتر میشود.
- طراحی کد بهبود یافته: رویکرد کاربر-محور RTL اغلب منجر به طراحی بهتر کامپوننت میشود، زیرا شما مجبور میشوید به نحوه تعامل کاربران با کامپوننتهای خود فکر کنید.
- جامعه و اکوسیستم: RTL دارای یک جامعه بزرگ و فعال است که منابع، پشتیبانی و افزونههای فراوانی را فراهم میکند.
راهاندازی محیط تست شما
برای شروع کار با RTL، باید محیط تست خود را راهاندازی کنید. در اینجا یک راهاندازی اولیه با استفاده از Create React App (CRA) آمده است که Jest و RTL را به صورت از پیش پیکربندی شده دارد:
npx create-react-app my-react-app
cd my-react-app
npm install --save-dev @testing-library/react @testing-library/jest-dom
توضیح:
- `npx create-react-app my-react-app`: یک پروژه جدید React با استفاده از Create React App ایجاد میکند.
- `cd my-react-app`: به داخل دایرکتوری پروژه تازه ایجاد شده میرود.
- `npm install --save-dev @testing-library/react @testing-library/jest-dom`: بستههای لازم RTL را به عنوان وابستگیهای توسعه نصب میکند. `@testing-library/react` قابلیتهای اصلی RTL را فراهم میکند، در حالی که `@testing-library/jest-dom` تطبیقدهندههای (matchers) مفید Jest را برای کار با DOM ارائه میدهد.
اگر از CRA استفاده نمیکنید، باید Jest و RTL را به طور جداگانه نصب کرده و Jest را برای استفاده از RTL پیکربندی کنید.
بهترین شیوهها برای تست کامپوننت با کتابخانه تست React
۱. تستهایی بنویسید که شبیه به تعاملات کاربر باشند
اصل اساسی RTL این است که کامپوننتها را همانطور که یک کاربر استفاده میکند، تست کنید. این به معنای تمرکز بر روی چیزی است که کاربر میبیند و انجام میدهد، نه جزئیات پیادهسازی داخلی. از شیء `screen` که توسط RTL ارائه شده است برای جستجوی عناصر بر اساس متن، نقش یا برچسبهای دسترسیپذیری آنها استفاده کنید.
مثال: تست کلیک روی یک دکمه
فرض کنید یک کامپوننت دکمه ساده دارید:
// Button.js
import React from 'react';
function Button({ onClick, children }) {
return ;
}
export default Button;
در اینجا نحوه تست آن با استفاده از RTL آمده است:
// Button.test.js
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import Button from './Button';
describe('Button Component', () => {
it('calls the onClick handler when clicked', () => {
const handleClick = jest.fn();
render();
const buttonElement = screen.getByText('Click Me');
fireEvent.click(buttonElement);
expect(handleClick).toHaveBeenCalledTimes(1);
});
});
توضیح:
- `render()`: کامپوننت Button را با یک کنترلکننده `onClick` شبیهسازی شده (mock) رندر میکند.
- `screen.getByText('Click Me')`: سند را برای عنصری که حاوی متن "Click Me" است، جستجو میکند. این همان روشی است که یک کاربر دکمه را شناسایی میکند.
- `fireEvent.click(buttonElement)`: یک رویداد کلیک را روی عنصر دکمه شبیهسازی میکند.
- `expect(handleClick).toHaveBeenCalledTimes(1)`: تأیید میکند که کنترلکننده `onClick` یک بار فراخوانی شده است.
چرا این روش بهتر از تست جزئیات پیادهسازی است: تصور کنید کامپوننت Button را برای استفاده از یک کنترلکننده رویداد دیگر یا تغییر state داخلی بازنویسی میکنید. اگر تابع کنترلکننده رویداد خاصی را تست میکردید، تست شما با شکست مواجه میشد. با تمرکز بر تعامل کاربر (کلیک کردن روی دکمه)، تست حتی پس از بازنویسی کد نیز معتبر باقی میماند.
۲. کوئریها را بر اساس قصد کاربر اولویتبندی کنید
RTL روشهای مختلف کوئری را برای یافتن عناصر فراهم میکند. کوئریهای زیر را به این ترتیب اولویتبندی کنید، زیرا آنها به بهترین شکل نحوه درک و تعامل کاربران با کامپوننتهای شما را منعکس میکنند:
- getByRole: این کوئری بیشترین دسترسیپذیری را دارد و باید اولین انتخاب شما باشد. این به شما امکان میدهد تا عناصر را بر اساس نقشهای ARIA آنها پیدا کنید (مانند button, link, heading).
- getByLabelText: از این برای یافتن عناصری که با یک برچسب خاص مرتبط هستند، مانند فیلدهای ورودی، استفاده کنید.
- getByPlaceholderText: از این برای یافتن فیلدهای ورودی بر اساس متن جایگزین (placeholder) آنها استفاده کنید.
- getByText: از این برای یافتن عناصر بر اساس محتوای متنی آنها استفاده کنید. دقیق باشید و از استفاده از متنهای عمومی که ممکن است در چندین مکان ظاهر شوند، خودداری کنید.
- getByDisplayValue: از این برای یافتن فیلدهای ورودی بر اساس مقدار فعلی آنها استفاده کنید.
مثال: تست یک ورودی فرم
// Input.js
import React from 'react';
function Input({ label, placeholder, value, onChange }) {
return (
);
}
export default Input;
در اینجا نحوه تست آن با استفاده از ترتیب کوئری پیشنهادی آمده است:
// Input.test.js
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import Input from './Input';
describe('Input Component', () => {
it('updates the value when the user types', () => {
const handleChange = jest.fn();
render();
const inputElement = screen.getByLabelText('Name');
fireEvent.change(inputElement, { target: { value: 'John Doe' } });
expect(handleChange).toHaveBeenCalledTimes(1);
expect(handleChange).toHaveBeenCalledWith(expect.objectContaining({ target: { value: 'John Doe' } }));
});
});
توضیح:
- `screen.getByLabelText('Name')`: از `getByLabelText` برای یافتن فیلد ورودی مرتبط با برچسب "Name" استفاده میکند. این روش، قابل دسترسترین و کاربرپسندانهترین راه برای مکانیابی ورودی است.
۳. از تست جزئیات پیادهسازی خودداری کنید
همانطور که قبلاً ذکر شد، از تست state داخلی، فراخوانی توابع یا کلاسهای CSS خاص خودداری کنید. اینها جزئیات پیادهسازی هستند که ممکن است تغییر کنند و میتوانند منجر به تستهای شکننده شوند. بر روی رفتار قابل مشاهده کامپوننت تمرکز کنید.
مثال: از تست مستقیم State خودداری کنید
به جای تست اینکه آیا یک متغیر state خاص بهروزرسانی شده است، تست کنید که آیا کامپوننت خروجی صحیح را بر اساس آن state رندر میکند. به عنوان مثال، اگر یک کامپوننت پیامی را بر اساس یک متغیر state بولین نمایش میدهد، تست کنید که آیا پیام نمایش داده میشود یا پنهان میشود، نه اینکه خود متغیر state را تست کنید.
۴. از `data-testid` برای موارد خاص استفاده کنید
در حالی که به طور کلی بهتر است از استفاده از ویژگیهای `data-testid` خودداری کنید، موارد خاصی وجود دارد که میتوانند مفید باشند:
- عناصری که معنای معنایی ندارند: اگر نیاز به هدف قرار دادن عنصری دارید که نقش، برچسب یا متن معناداری ندارد، میتوانید از `data-testid` استفاده کنید.
- ساختارهای پیچیده کامپوننت: در ساختارهای پیچیده کامپوننت، `data-testid` میتواند به شما کمک کند تا عناصر خاصی را بدون تکیه بر انتخابگرهای شکننده هدف قرار دهید.
- تست دسترسیپذیری: `data-testid` میتواند برای شناسایی عناصر خاص در طول تست دسترسیپذیری با ابزارهایی مانند Cypress یا Playwright استفاده شود.
مثال: استفاده از `data-testid`
// MyComponent.js
import React from 'react';
function MyComponent() {
return (
This is my component.
);
}
export default MyComponent;
// MyComponent.test.js
import React from 'react';
import { render, screen } from '@testing-library/react';
import MyComponent from './MyComponent';
describe('MyComponent', () => {
it('renders the component container', () => {
render( );
const containerElement = screen.getByTestId('my-component-container');
expect(containerElement).toBeInTheDocument();
});
});
مهم: از `data-testid` به ندرت و فقط زمانی که سایر روشهای کوئری مناسب نیستند، استفاده کنید.
۵. توضیحات تست معنادار بنویسید
توضیحات تست واضح و مختصر برای درک هدف هر تست و برای اشکالزدایی از شکستها بسیار مهم است. از نامهای توصیفی استفاده کنید که به وضوح توضیح میدهند تست چه چیزی را تأیید میکند.
مثال: توضیحات تست خوب در مقابل بد
بد: `it('works')`
خوب: `it('displays the correct greeting message')`
حتی بهتر: `it('displays the greeting message "Hello, World!" when the name prop is not provided')`
مثال بهتر به وضوح رفتار مورد انتظار کامپوننت را تحت شرایط خاص بیان میکند.
۶. تستهای خود را کوچک و متمرکز نگه دارید
هر تست باید بر روی تأیید یک جنبه از رفتار کامپوننت تمرکز کند. از نوشتن تستهای بزرگ و پیچیده که سناریوهای متعددی را پوشش میدهند، خودداری کنید. تستهای کوچک و متمرکز برای درک، نگهداری و اشکالزدایی آسانتر هستند.
۷. از بدلهای تست (ماکها و جاسوسها) به طور مناسب استفاده کنید
بدلهای تست برای جداسازی کامپوننتی که در حال تست آن هستید از وابستگیهایش مفید هستند. از ماکها و جاسوسها برای شبیهسازی سرویسهای خارجی، فراخوانیهای API یا سایر کامپوننتها استفاده کنید.
مثال: شبیهسازی یک فراخوانی API
// UserList.js
import React, { useState, useEffect } from 'react';
function UserList() {
const [users, setUsers] = useState([]);
useEffect(() => {
async function fetchUsers() {
const response = await fetch('/api/users');
const data = await response.json();
setUsers(data);
}
fetchUsers();
}, []);
return (
{users.map(user => (
- {user.name}
))}
);
}
export default UserList;
// UserList.test.js
import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import UserList from './UserList';
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve([
{ id: 1, name: 'John Doe' },
{ id: 2, name: 'Jane Smith' },
]),
})
);
describe('UserList Component', () => {
it('fetches and displays a list of users', async () => {
render( );
// Wait for the data to load
await waitFor(() => screen.getByText('John Doe'));
expect(screen.getByText('John Doe')).toBeInTheDocument();
expect(screen.getByText('Jane Smith')).toBeInTheDocument();
});
});
توضیح:
- `global.fetch = jest.fn(...)`: تابع `fetch` را برای بازگرداندن یک لیست از پیش تعریف شده از کاربران شبیهسازی میکند. این به شما امکان میدهد تا کامپوننت را بدون تکیه بر یک نقطه پایانی API واقعی تست کنید.
- `await waitFor(() => screen.getByText('John Doe'))`: منتظر میماند تا متن "John Doe" در سند ظاهر شود. این کار ضروری است زیرا دادهها به صورت ناهمزمان واکشی میشوند.
۸. موارد مرزی و مدیریت خطا را تست کنید
فقط مسیر خوشحال (happy path) را تست نکنید. حتماً موارد مرزی، سناریوهای خطا و شرایط حدی را تست کنید. این به شما کمک میکند تا مشکلات بالقوه را در مراحل اولیه شناسایی کرده و اطمینان حاصل کنید که کامپوننت شما موقعیتهای غیرمنتظره را به خوبی مدیریت میکند.
مثال: تست مدیریت خطا
تصور کنید کامپوننتی دادهها را از یک API واکشی میکند و در صورت شکست فراخوانی API، یک پیام خطا نمایش میدهد. شما باید تستی بنویسید تا تأیید کنید که پیام خطا در هنگام شکست فراخوانی API به درستی نمایش داده میشود.
۹. بر دسترسیپذیری تمرکز کنید
دسترسیپذیری برای ایجاد برنامههای وب فراگیر بسیار مهم است. از RTL برای تست دسترسیپذیری کامپوننتهای خود و اطمینان از مطابقت آنها با استانداردهای دسترسیپذیری مانند WCAG استفاده کنید. برخی از ملاحظات کلیدی دسترسیپذیری عبارتند از:
- HTML معنایی: از عناصر HTML معنایی (مانند `
- ویژگیهای ARIA: از ویژگیهای ARIA برای ارائه اطلاعات اضافی در مورد نقش، وضعیت و خواص عناصر، به ویژه برای کامپوننتهای سفارشی، استفاده کنید.
- ناوبری با صفحه کلید: اطمینان حاصل کنید که تمام عناصر تعاملی از طریق ناوبری با صفحه کلید قابل دسترسی هستند.
- کنتراست رنگ: از کنتراست رنگ کافی برای اطمینان از خوانا بودن متن برای کاربران با دید کم استفاده کنید.
- سازگاری با صفحهخوان: کامپوننتهای خود را با یک صفحهخوان تست کنید تا اطمینان حاصل کنید که تجربه معنادار و قابل درکی را برای کاربران با اختلالات بینایی فراهم میکنند.
مثال: تست دسترسیپذیری با `getByRole`
// MyAccessibleComponent.js
import React from 'react';
function MyAccessibleComponent() {
return (
);
}
export default MyAccessibleComponent;
// MyAccessibleComponent.test.js
import React from 'react';
import { render, screen } from '@testing-library/react';
import MyAccessibleComponent from './MyAccessibleComponent';
describe('MyAccessibleComponent', () => {
it('renders an accessible button with the correct aria-label', () => {
render( );
const buttonElement = screen.getByRole('button', { name: 'Close' });
expect(buttonElement).toBeInTheDocument();
});
});
توضیح:
- `screen.getByRole('button', { name: 'Close' })`: از `getByRole` برای یافتن یک عنصر دکمه با نام قابل دسترس "Close" استفاده میکند. این تضمین میکند که دکمه به درستی برای صفحهخوانها برچسبگذاری شده است.
۱۰. تست را در گردش کار توسعه خود ادغام کنید
تست باید بخش جداییناپذیر گردش کار توسعه شما باشد، نه یک فکر بعدی. تستهای خود را در خط لوله CI/CD خود ادغام کنید تا هر زمان که کد کامیت یا دیپلوی میشود، تستها به طور خودکار اجرا شوند. این به شما کمک میکند تا باگها را زودتر پیدا کرده و از رگرسیون جلوگیری کنید.
۱۱. بومیسازی و بینالمللیسازی (i18n) را در نظر بگیرید
برای برنامههای جهانی، در نظر گرفتن بومیسازی و بینالمللیسازی (i18n) در طول تست بسیار مهم است. اطمینان حاصل کنید که کامپوننتهای شما به درستی در زبانها و مناطق مختلف رندر میشوند.
مثال: تست بومیسازی
اگر از کتابخانهای مانند `react-intl` یا `i18next` برای بومیسازی استفاده میکنید، میتوانید زمینه بومیسازی را در تستهای خود شبیهسازی کنید تا تأیید کنید که کامپوننتهای شما متن ترجمه شده صحیح را نمایش میدهند.
۱۲. از توابع رندر سفارشی برای راهاندازی قابل استفاده مجدد استفاده کنید
هنگام کار بر روی پروژههای بزرگتر، ممکن است متوجه شوید که مراحل راهاندازی یکسانی را در چندین تست تکرار میکنید. برای جلوگیری از تکرار، توابع رندر سفارشی ایجاد کنید که منطق راهاندازی مشترک را کپسوله میکنند.
مثال: تابع رندر سفارشی
// test-utils.js
import React from 'react';
import { render } from '@testing-library/react';
import { ThemeProvider } from 'styled-components';
import theme from './theme';
const AllTheProviders = ({ children }) => {
return (
{children}
);
}
const customRender = (ui, options) =>
render(ui, { wrapper: AllTheProviders, ...options })
// re-export everything
export * from '@testing-library/react'
// override render method
export { customRender as render }
// MyComponent.test.js
import React from 'react';
import { render, screen } from './test-utils'; // Import the custom render
import MyComponent from './MyComponent';
describe('MyComponent', () => {
it('renders correctly with the theme', () => {
render( );
// Your test logic here
});
});
این مثال یک تابع رندر سفارشی ایجاد میکند که کامپوننت را با یک ThemeProvider میپوشاند. این به شما امکان میدهد تا به راحتی کامپوننتهایی را که به تم وابسته هستند، بدون نیاز به تکرار راهاندازی ThemeProvider در هر تست، تست کنید.
نتیجهگیری
کتابخانه تست React یک رویکرد قدرتمند و کاربر-محور برای تست کامپوننت ارائه میدهد. با پیروی از این بهترین شیوهها، میتوانید تستهای قابل نگهداری و مؤثری بنویسید که بر رفتار کاربر و دسترسیپذیری تمرکز دارند. این منجر به برنامههای React قویتر، قابل اعتمادتر و فراگیرتر برای مخاطبان جهانی خواهد شد. به یاد داشته باشید که تعاملات کاربر را در اولویت قرار دهید، از تست جزئیات پیادهسازی خودداری کنید، بر دسترسیپذیری تمرکز کنید و تست را در گردش کار توسعه خود ادغام کنید. با پذیرش این اصول، میتوانید برنامههای React با کیفیت بالا بسازید که نیازهای کاربران در سراسر جهان را برآورده میکنند.
نکات کلیدی:
- تمرکز بر تعاملات کاربر: کامپوننتها را همانطور که یک کاربر با آنها تعامل میکند، تست کنید.
- اولویتبندی دسترسیپذیری: اطمینان حاصل کنید که کامپوننتهای شما برای کاربران دارای معلولیت قابل دسترس هستند.
- اجتناب از جزئیات پیادهسازی: state داخلی یا فراخوانی توابع را تست نکنید.
- نوشتن تستهای واضح و مختصر: تستهای خود را برای درک و نگهداری آسان کنید.
- ادغام تست در گردش کار خود: تستهای خود را خودکار کرده و به طور منظم اجرا کنید.
- در نظر گرفتن مخاطبان جهانی: اطمینان حاصل کنید که کامپوننتهای شما در زبانها و مناطق مختلف به خوبی کار میکنند.