الوحدة 5 · الإطلاق
الاختبار باستخدام Jest وReact Native Testing Library
18 دقراءة 3 داملأ الفراغات
الدرس في لمحة
اضبط Jest مع الـ preset المسمّى jest-expo، واكتب unit tests لمنطق Trailhead النقي، واختبر الـ components بالطريقة التي يستخدمها بها المستخدمون عبر React Native Testing Library، واصنع mock للوحدات الأصلية عند الحدود.
- 1إعداد Jest
- 2ابدأ بالمنطق النقي
- 3اختبار الـ components كما يستخدمها المستخدم
- 4صنع mock للجهاز عند الحدود
- 5أين تقع اختبارات end-to-end
لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.
دفع كل درس في هذه الدورة المنطقَ خارج الـ components إلى دوال نقية صغيرة: parseDistanceKm، وhikesReducer، وvalidateHikeForm، وpermissionAction، وtrackDistanceKm. ولم يكن ذلك من أجل الترتيب فقط. على الجوّال، التحقّق من تغيير يدويًا يعني إعادة البناء، والتشغيل، والتنقّل ثلاث شاشات إلى الداخل، والكتابة على لوحة مفاتيح محاكٍ. واختبار يتحقّق من الشيء نفسه في 20 جزءًا من الألف من الثانية، في كل مرة تحفظ فيها، أفضل مقايضة ستعقدها.
إعداد Jest
يضبط الـ preset الخاص بـ Expo، jest-expo، أداة Jest لـ React Native: يحوّل كود TypeScript وJSX، ويصنع mock للجانب الأصلي من وحدات Expo، فتعمل الاختبارات في Node دون جهاز. ثبّته مع React Native Testing Library:
npx expo install jest-expo jest @types/jest --dev
npx expo install @testing-library/react-native --devثم وجّه Jest إلى الـ preset في package.json:
{
"scripts": {
"test": "jest --watchAll"
},
"jest": {
"preset": "jest-expo"
}
}يذكر دليل Expo أيضًا مُدخلًا لـ transformIgnorePatterns للحزم التي تُشحن بكود غير محوَّل؛ انسخه من هناك حين يفشل استيراد برسالة “SyntaxError: Cannot use import statement outside a module”. وضع الاختبارات في مجلّدات __tests__ بجوار الكود الذي تغطّيه.
ابدأ بالمنطق النقي
لأداة التحليل من القسم 1 قواعد صغيرة كثيرة. وجدول من الحالات يوثّقها أفضل من النثر:
import { parseDistanceKm } from '../parseDistance';
test.each([
['8.4', 8.4],
['8,4', 8.4],
[' 12 km', 12],
['abc', null],
['0', null],
['1.2.3', null],
])('parseDistanceKm(%j) is %p', (input, expected) => {
expect(parseDistanceKm(input)).toBe(expected);
});لاحظ أن حالات الاختبار هي بالضبط المدخلات الغريبة التي قلقت بشأنها حين كتبت الدالة: الفاصلة العشرية، والوحدة اللاحقة، والصفر. كل بلاغ عن خطأ يصبح صفًّا إضافيًا في جدول كهذا، يُكتب قبل الإصلاح، فلا يستطيع الخطأ نفسه العودة بصمت أبدًا.
والـ reducers تُعامَل بالطريقة نفسها: ابنِ state، وأرسل action، وتحقّق من النتيجة، وتحقّق أن المدخل لم يُعدَّل. هذه الاختبارات سريعة ومستقرّة ولا تحتاج إلى mocks، ولهذا يجب أن تشكّل معظم مجموعة اختباراتك.
اختبار الـ components كما يستخدمها المستخدم
ترسم React Native Testing Library الـ components في الذاكرة، وتتيح لك الاستعلام عنها كما يدركها الناس: بالـ role والـ label والنص. هذا اختبار للـ component المسمّى QuickLog من القسم 1:
import { render, screen, userEvent } from '@testing-library/react-native';
import { QuickLog } from '../QuickLog';
test('saves a valid distance', async () => {
const onSave = jest.fn();
const user = userEvent.setup();
await render(<QuickLog onSave={onSave} />);
await user.type(screen.getByLabelText('Distance (km)'), '8,4');
await user.press(screen.getByRole('button', { name: 'Save' }));
expect(onSave).toHaveBeenCalledWith(8.4);
});
test('keeps Save disabled until the distance is valid', async () => {
const user = userEvent.setup();
await render(<QuickLog onSave={jest.fn()} />);
await user.type(screen.getByLabelText('Distance (km)'), 'far');
expect(screen.getByRole('button', { name: 'Save' })).toBeDisabled();
});لكي يعثر getByLabelText على الحقل، يحتاج الـ TextInput إلى accessibilityLabel="Distance (km)". وهذا بيت القصيد: الاختبار الذي يستعلم بالـ role والـ label يفشل حين تكون الشاشة غير قابلة للوصول، فتصبح اختباراتك فحصًا لإمكانية الوصول أيضًا. يحاكي userEvent التفاعلات الحقيقية (التركيز، وكتابة كل حرف، والضغط والإفلات)، وفي الإصدارات الحالية من المكتبة يكون كلٌّ من render والتفاعلات غير متزامن، فانتظرها بـ await. والـ matchers مثل toBeOnTheScreen() وtoHaveTextContent() وtoBeDisabled() تأتي مع المكتبة.
صنع mock للجهاز عند الحدود
يضع jest-expo بدائل للوحدات الأصلية، لكن هذه البدائل لا تُرجع شيئًا مفيدًا. وحين يعتمد component على إجابة وحدة ما، اصنع لها mock بالإجابة التي تريد اختبارها:
import * as Location from 'expo-location';
jest.mock('expo-location', () => ({
getForegroundPermissionsAsync: jest.fn(),
requestForegroundPermissionsAsync: jest.fn(),
}));
test('offers Settings after a permanent denial', async () => {
jest.mocked(Location.getForegroundPermissionsAsync).mockResolvedValue({
status: 'denied', granted: false, canAskAgain: false, expires: 'never',
} as Location.LocationPermissionResponse);
await render(<TrackScreen />);
expect(await screen.findByRole('button', { name: 'Open Settings' })).toBeOnTheScreen();
});استعلامات findBy… تنتظر ظهور العنصر، وهذا يناسب الشاشات التي تحمّل بشكل غير متزامن. اصنع الـ mocks عند حافة تطبيقك (الوحدات الأصلية، وfetch)، لا للـ components الخاصة بك، ليكون الكود قيد الاختبار هو الكود الذي تطلقه. والشاشات التي تستخدم hooks من Expo Router يمكن رسمها باستخدام renderRouter من expo-router/testing-library، الذي يجهّز router في الذاكرة بالمسارات التي تعطيه إياها.
أين تقع اختبارات end-to-end
لا تستطيع اختبارات الوحدات واختبارات الـ components التقاط build أصلي معطوب، أو نافذة إذن لا تظهر أبدًا، أو لوحة مفاتيح تغطّي زرّ Save. بضعة اختبارات end-to-end على builds حقيقية تغطّي ذلك: أدوات مثل Maestro (التي يستطيع EAS تشغيلها في السحابة) أو Detox تقود التطبيق الفعلي على محاكٍ. احتفظ بها للمسارات التي سيؤلم كسرها أكثر من غيرها، مثل تسجيل الدخول وتسجيل رحلة، ودع الاختبارات السريعة تغطّي كل ما عدا ذلك.
التالي: تحويل التطبيق المختبَر إلى builds تستطيع الهواتف الحقيقية تثبيتها.
أهم النقاط
- يضبط الـ preset المسمّى jest-expo أداة Jest لـ React Native، ويصنع mock للجانب الأصلي من وحدات Expo، فتعمل الاختبارات في Node دون جهاز.
- الدوال النقية مثل أدوات التحليل والـ reducers ودوال التحقّق تعطي أكبر قدر من الثقة مقابل كل دقيقة من كتابة الاختبارات؛ فاختبرها أولًا.
- ترسم React Native Testing Library الـ components وتتيح لك العثور على العناصر بالدور (role) والتسمية (label) والنص، كما يفعل المستخدمون وقارئات الشاشة.
- في الإصدارات الحالية من المكتبة،
renderوتفاعلاتuserEventغير متزامنة؛ فانتظرها بـ await. - اصنع mock للوحدات الأصلية مثل expo-location عند الحدود باستخدام
jest.mock، واكتب الـ assertions على ما يراه المستخدم.