الگوی آزمایش HRR React – انجمن DEV

در زیر راهنمای مختصری برای نتیجه رندر هوک (HRR) الگو، با استفاده از مثال یک قلاب سفارشی به نام useFetchUsers
. هدف این سند این است که شما را از طریق ایده های اصلی راهنمایی کند: چی الگو این است، چرا مفید است، و چگونه تا آن را در پایگاه کد خود پیاده سازی کنید.
نمای کلی
هنگام آزمایش یک React Hook سفارشی (به عنوان مثال، useFetchUsers
)، می توانیم به کتابخانه React Testing و آن تکیه کنیم renderHook
API. با این حال، پس از رندر کردن قلاب، شما اغلب در پایان تماس می گیرید result.current
و بسته بندی دستی عملیات خاص در act()
. این الگوی HRR این اقدامات رایج را در یک شیء کمکی جداگانه متمرکز می کند تا آزمایشات شما تمیزتر و گویاتر باقی بمانند.
قبل و بعد
یک تست معمولی با استفاده از renderHook
API ممکن است به شکل زیر باشد:
test('when fetch: should set loading and users', async () => {
// Arrange
const myMockFetch = jest.fn().mockResolvedValue([{ id: '1', name: 'User 1' }]);
const { result } = renderHook(() => useFetchUsers(myMockFetch));
// Assert initial state
expect(result.current.isLoading).toBe(false);
expect(result.current.users).toEqual([]);
// Act
await act(async () => {
await result.current.fetch();
});
// Assert final state
expect(myMockFetch).toHaveBeenCalledTimes(1);
expect(result.current.isLoading).toBe(false);
expect(result.current.users).toEqual([{ id: '1', name: 'User 1' }]);
});
هنگامی که الگوی HRR پیاده سازی شد، نوشتن تست ها راحت تر خواهد بود. در اینجا نمونه ای از این است که همان تست بالا شبیه آن است:
test('when fetch: should set loading and users', async () => {
// Arrange
fetchUsers = renderUseFetchUsers();
expect(fetchUsers.isLoading).toBe(false);
expect(fetchUsers.users).toEqual([]);
// Act
await fetchUsers.fetch();
// Assert
expect(myMockFetch).toHaveBeenCalledTimes(1);
expect(fetchUsers.isLoading).toBe(false);
expect(fetchUsers.users).toEqual([{ id: '1', name: 'User 1' }]);
});
سه فایل
مثال سه فایل را نشان می دهد:
-
قلاب (
useFetchUsers.ts
):منطق اصلی را برای واکشی و ذخیره لیستی از کاربران پیاده سازی می کند. این قلاب در حال آزمایش است.
-
یاور آزمون (
UseFetchUsersRenderResult.ts
):نتیجه قلاب رندر شده را میپیچد تا یک API دوستانهتر برای ادعاهای آزمایشی شما نشان دهد (به عنوان مثال، نیازی به تماس مکرر نیست
result.current
یا به صورت دستی هر اقدامی را در آن قرار دهیدact()
). در حالی که میخواهید این کمککننده تا جایی که میتواند به API هوک نزدیک باشد، وظیفه آن آسانتر کردن آزمایشها است – بنابراین هنگام معرفی تغییرات، تصمیمگیری آگاهانه بگیرید. -
فایل تست (
useFetchUsers.test.ts
):نحوه استفاده از کلاس کمکی را در آزمونهای خود نشان میدهد، به جای دیگ بخار، بر منطق قلاب تمرکز میکند.
در زیر، هر فایل را مرور می کنیم و جزئیات کلیدی این الگو را برجسته می کنیم.
1. قلاب سفارشی
نکات کلیدی
-
مدیریت دولتی: قلاب یک را دنبال می کند
isLoading
پرچم وusers
. -
واکشی: یک ناهمگام تابعی که حالت بارگیری را راه اندازی می کند، فراخوانی می کند
fetchUsers
آرگومان وابستگی و بهروزرسانیها پس از تکمیل وضعیت میشوند.
// useFetchUsers.ts
import React, { useState } from 'react';
export interface User {
id: string;
name: string;
}
type FetchUsersFn = () => Promise<User[]>;
export interface UseFetchUsersResult {
isLoading: boolean;
users: User[];
fetch: () => Promise<void>;
}
const useFetchUsers = (fetchUsers: FetchUsersFn, messaging: (message: string) => void): UseFetchUsersResult => {
const [isLoading, setIsLoading] = useState(false);
const [users, setUsers] = useState<User[]>([]);
const fetch = async () => {
setIsLoading(true);
try {
const users = await fetchUsers();
setUsers(users);
} catch {
messaging('There was an issue fetching users');
} finally {
setIsLoading(false);
}
};
return {
isLoading,
users,
fetch,
};
};
export default useFetchUsers;
2. Hook Render Result Helper
نکات کلیدی
-
همین رابط را پیاده سازی می کند (
UseFetchUsersResult
) به عنوان قلاب سفارشی، بنابراین می توان آن را به جای یکدیگر مورد استفاده قرار داد (به عنوان مثال،isLoading
،users
، وfetch
). -
کپسول می کند
act()
: هنگام تماسfetch()
، به طور خودکار قلاب را می پیچدfetch
در یکReact Test Utils
act()
تماس بگیرید. این تضمین می کند که به روز رسانی های حالت به درستی و بدون پراکندگی اعمال می شوندact()
تماس در سراسر آزمون شما.
در اصل، این کلاس کمکی یک API کنترل شده را نشان می دهد برای تعامل با وضعیت و روشهای قلاب، تمرکز کد تست بر روی رفتارها به جای دیگ بخار. به یاد داشته باشید، این راهنما اینجاست تا آزمایش را آسانتر کند، بنابراین اگر تغییراتی در مورد استفاده شما وجود دارد، تصمیم آگاهانهای در مورد نحوه راهاندازی کمککننده خود بگیرید.
// UseFetchUsersRenderResult.ts
import { act } from 'react-dom/test-utils';
import { UseFetchUsersResult } from '../../useFetchUsers';
type HookRenderResult = { current: UseFetchUsersResult };
class UseFetchUsersRenderResult implements UseFetchUsersResult {
constructor(private renderResult: HookRenderResult) {}
get isLoading() {
return this.renderResult.current.isLoading;
}
get users() {
return this.renderResult.current.users;
}
async fetch() {
await act(async () => {
await this.renderResult.current.fetch();
});
}
}
export default UseFetchUsersRenderResult;
3. فایل تست
راه اندازی
-
DEFAULT_ARGS
: آرگومان های پیش فرض را برای the تعریف می کندfetchUsers
وmessaging
توابعی که باید در طول آزمایش مورد تمسخر قرار گیرند. آزمایشهای فردی میتوانند این پیشفرضها را برای شبیهسازی سناریوهای مختلف لغو کنند (مثلاً واکشی موفق، خطاها، پاسخهای با تأخیر). -
renderUseFetchUsers
: یک تابع کاربردی کوچک که از الگوی HRR ما استفاده می کند. تماس می گیردrenderHook(() => useFetchUsers(...))
، سپس برگردانده را می پیچدresult
در ماUseFetchUsersRenderResult
کلاس کمکی
// useFetchUsers.test.ts
import { renderHook } from '@testing-library/react';
import useFetchUsers, { User } from '../useFetchUsers';
import UseFetchUsersRenderResult from './helpers/UseFetchUsersRenderResult';
const DEFAULT_ARGS = Object.freeze({
fetchUsers: jest.fn(),
messaging: jest.fn(),
});
const renderUseFetchUsers = (fetchUsers = DEFAULT_ARGS.fetchUsers, messaging = DEFAULT_ARGS.messaging): UseFetchUsersRenderResult => {
const { result } = renderHook(() => useFetchUsers(fetchUsers, messaging));
return new UseFetchUsersRenderResult(result);
};
describe('useFetchUsers', () => {
let fetchUsers: UseFetchUsersRenderResult;
afterEach(() => {
jest.clearAllMocks();
});
test('when initializing: should have correct default result', async () => {
// Arrange, act
fetchUsers = renderUseFetchUsers();
// Assert
expect(fetchUsers.isLoading).toBe(false);
expect(fetchUsers.users).toEqual([]);
});
test('when fetch: should call fetchUsers arg', async () => {
// Arrange
fetchUsers = renderUseFetchUsers();
// Act
await fetchUsers.fetch();
// Assert
expect(DEFAULT_ARGS.fetchUsers).toHaveBeenCalledTimes(1);
});
test('when fetch: should set loading and users', async () => {
// Arrange
const myMockFetch = jest.fn().mockImplementation(
() =>
new Promise<User[]>((resolve) => {
setTimeout(() => {
resolve([{ id: '1', name: 'User 1' }]);
}, 50);
})
);
fetchUsers = renderUseFetchUsers(myMockFetch);
// Act
await fetchUsers.fetch();
// Assert
expect(fetchUsers.isLoading).toBe(false);
expect(fetchUsers.users).toEqual([{ id: '1', name: 'User 1' }]);
});
test('when fetch: should set loading and call messaging on error', async () => {
// Arrange
const myMockFetch = jest.fn().mockRejectedValueOnce(new Error('An error occurred'));
fetchUsers = renderUseFetchUsers(myMockFetch);
// Act
await fetchUsers.fetch();
// Assert
expect(DEFAULT_ARGS.messaging).toHaveBeenCalledTimes(1);
expect(DEFAULT_ARGS.messaging).toHaveBeenCalledWith('There was an issue fetching users');
});
});
نتیجه گیری الگو
این نتیجه رندر هوک (HRR) الگو روشی ساده برای آزمایش قلاب های سفارشی شما با متمرکز کردن دیگ بخار معمولی در یک کلاس کمکی ارائه می دهد. به جای تماس مکرر result.current
و به صورت دستی هر عمل را در آن قرار دهید act()
، کمک کننده این جزئیات را برای شما مدیریت می کند. این باعث میشود روی تستهای شما متمرکز شوند چی قلاب باید به جای چگونه برای هماهنگ کردن آن اقدامات در محیط آزمایش.
مدیریت منطق Async در Helper شما
جاوا اسکریپت اغلب اوقات نیاز به مدیریت عملکرد ناهمگام دارد. وقتی قلاب شما یک تابع ناهمگام را برمی گرداند—مانند fetch()
در ما useFetchUsers
به عنوان مثال – ممکن است بخواهید آزمایش کنید حالت های میانی که بین شروع و پایان عملکرد ناهمگام شما اتفاق می افتد (به عنوان مثال، بررسی isLoading
بلافاصله پس از شروع تماس). در زیر روشهای رایجی وجود دارد که میتوانید در کلاس کمکی خود ترکیب و مطابقت دهید.
1. تماس Async “همه در یک”.
رویکرد مورد استفاده در مثال های بالا. این ساده ترین است، نکته این است که کل تماس غیر همگام (از جمله تماس) را بپیچید .then()
یا await
) داخل یک act(async () => {})
. به عبارت دیگر، اگر یاور شما یک مجرد را افشا کند fetch()
روش، آن را داشته باشید در انتظار روش واکشی قلاب در داخل act
:
async fetch() {
await act(async () => {
await this.renderResult.current.fetch();
});
}
جوانب مثبت:
- کد تست باقی می ماند بسیار ساده-فقط
await fetchUsers.fetch()
و حالات نهایی را بیان کنید. - بدون نیاز به ابزارهای آزمایشی اضافی (به عنوان مثال، بدون نیاز به
waitFor
یا تقسیم مراحل).
منفی:
- شما نمی تواند حالت میانی را آزمایش کند – هیچ راهی برای اثبات این موضوع وجود ندارد
isLoading
بودtrue
بلافاصله پس از واکشی شروع می شود، زیرا در آن زمانfetch()
حل می شود، قلاب قبلاً به روز رسانی به پایان رسیده است.
2. تماس Async «شروع» را در مقابل «پایان» تقسیم کنید
اگر می خواهید این را ادعا کنید isLoading
است true
قبل از واکشی حل می شود، تقسیم جریان را در نظر بگیرید. این رویکرد روشی ساختاریافته برای کنترل و به تاخیر انداختن زمانی که حل و فصل یک وعده را بررسی می کنید ارائه می دهد. به عنوان مثال، کلاس helper یک خواهد داشت startFetch()
روشی برای شروع تماس async و یک روش جداگانه finishFetch()
روش انتظار برای تکمیل آن:
class UseFetchUsersRenderResult implements UseFetchUsersResult {
private fetchPromise: Promise<void> | null = null;
// ...other code
async fetch() {
await act(async () => {
await this.renderResult.current.fetch();
});
}
startFetch() {
act(() => {
this.fetchPromise = this.renderResult.current.fetch();
});
}
async finishFetch() {
if (!this.fetchPromise) throw new Error('fetch() not started.');
await act(async () => {
await this.fetchPromise;
});
this.fetchPromise = null;
}
}
چگونه با این تست کنیم:
test('should show isLoading during fetch', async () => {
const fetchUsers = renderUseFetchUsers();
// Kick off fetch
fetchUsers.startFetch();
// Immediately check transitional state
expect(fetchUsers.isLoading).toBe(true);
// Wait for completion
await fetchUsers.finishFetch();
// Post-fetch assertions
expect(fetchUsers.isLoading).toBe(false);
});
جوانب مثبت:
- به شما امکان تست می دهد کشورهای انتقالی درست پس از شروع واکشی
- کنترل صریح تر زمان بندی هر مرحله.
منفی:
- کمی پرمخاطب تر – دو روش فراخوانی (
startFetch
،finishFetch
) به جای یکی. - کلاس کمکی شما را از یک به یک بودن با API هوک شما دورتر می کند.
3. آزمایش یک تابع خالص به طور جداگانه
ممکن است بخواهید منطق واقعی واکشی (یعنی بدنه قلاب شما) را جدا کنید fetch
روش) به یک تابع خالص، مانند doFetch
، که وابستگی های خود را می پذیرد (به عنوان مثال، setIsLoading
، fetchUsers
، setUsers
، messaging
) به جای دسترسی مستقیم به React State. این کار کلاس کمکی شما را سادهتر نگه میدارد و به شما این امکان را میدهد که با رویکرد «همهدر یک» در حالی که حالت میانی را جداگانه آزمایش میکنید، رول کنید:
// useFetchUsers.ts
import { doFetch } from './doFetch';
const useFetchUsers = (fetchUsers: FetchUsersFn, messaging: (message: string) => void): UseFetchUsersResult => {
const [users, setUsers] = useState<User[]>([]);
const [isLoading, setIsLoading] = useState(false);
const fetch = async () => {
await doFetch({
setIsLoading,
fetchUsers,
setUsers,
messaging,
});
};
return {
users,
isLoading,
fetch,
};
};
نحوه تست کردن با این:
// doFetch.test.ts
import { doFetch } from './doFetch';
const DEFAULT_ARGS = Object.freeze({
setIsLoading = jest.fn(),
fetchUsers = jest.fn(),
setUsers = jest.fn(),
messaging = jest.fn()
});
describe('useFetchUsers - doFetch', () => {
afterEach(() => {
jest.clearAllMocks();
});
it('should set loading, fetch data, set users, and unset loading on success', async () => {
// Arrange
const mockUsers = [{ id: '1', name: 'User 1' }];
fetchUsers.mockResolvedValue(mockUsers);
// Act
await doFetch({
setIsLoading,
fetchUsers,
setUsers,
messaging,
});
// Assert
expect(DEFAULT_ARGS.setIsLoading).toHaveBeenNthCalledWith(1, true); // first call
expect(DEFAULT_ARGS.fetchUsers).toHaveBeenCalledTimes(1);
expect(DEFAULT_ARGS.setUsers).toHaveBeenCalledWith(mockUsers);
expect(DEFAULT_ARGS.messaging).not.toHaveBeenCalled();
expect(DEFAULT_ARGS.setIsLoading).toHaveBeenNthCalledWith(2, false); // final call
});
});
جوانب مثبت:
- به شما اجازه می دهد کلاس کمکی کوچک و متمرکز بمانید: با بیرون کشیدن منطق به یک تابع خالص، نیازی نیست که HRR را با مدیریت ناهمگام گام به گام شلوغ کنید.
-
جدا می کند کد واکشی برای آزمایش مستقل: تأیید کنید کشورهای انتقالی (مثلاً تنظیم
isLoading
بهtrue
) در یک محیط ساده تر بدون React.
منفی:
- اضافی اضافه می کند لایه انتزاعی: تقسیم کد به یک تابع جداگانه نیاز به تنظیمات بیشتری دارد.
از کدام گزینه استفاده کنیم؟
در پایان روز بستگی به این دارد چی می خواهید تست کنید و چگونه می خواهید آن را تست کنید:
- اگر بیشتر به آن اهمیت می دهید نتایج نهایی (به عنوان مثال، داده های بارگذاری شده در مقابل خطا)، تک
await fetch()
روش کاملاً خوب است - اگر شما نیاز دارید بررسی وضعیت های میانی، رویکرد تقسیم “شروع در برابر پایان” انعطاف پذیرتر است.
- اگر شما نیاز دارید بررسی وضعیت های میانی اما ترجیح می دهند a کلاس کمکی ساده تر (به قیمت برخی پیچیدگی های اضافی در قسمت داخلی قلاب شما)، در نظر بگیرید تست واحد منطق واکشی میانی به طور جداگانه به عنوان یک تابع خالص. به این ترتیب، تستهای هوک شما میتوانند بر روی انتقال حالت متمرکز شوند در حالی که منطق انتقالی را در جای دیگری بررسی میکنید.
افکار نهایی
تست همه چیز در مورد است کسب اعتماد به نفس که کد شما مطابق انتظار عمل می کند. ثابت ماندن نحوه نوشتن و سازماندهی آزمونهای خود راهی عالی برای افزایش این اعتماد به نفس است. چه الگوی HRR را بپذیرید یا به طور کامل از روش دیگری استفاده کنید، هدف نهایی ثابت می ماند: اطمینان حاصل کنید که تست های شما انجام می شود. قابل خواندن، قابل نگهداری، و به طور دقیق نیازهای برنامه شما را منعکس می کند. اگر بتوانید بهطور مداوم باگها را زودتر تشخیص دهید و از تغییرات مداوم احساس امنیت کنید، به آنچه در آزمایش اهمیت دارد، دست یافتهاید.