آلية اختبار التطبيقات في React Native
يمكن أن تتحول الأخطاء الصغيرة التي لا تتوقعها إلى حالات فشل أكبر مع توسع قاعدة الشيفرة، حيث تؤدي الأخطاء إلى تجربة مستخدم سيئة والتي ستؤدي في النهاية إلى خسائر مالية. تتمثل إحدى طرق منع البرمجة الضعيفة في اختبار شيفرتك قبل إصدارها.
سنغطّي طرقًا آلية مختلفة لضمان عمل تطبيقك كما هو متوقع بدءًا من التحليل الساكن static analysis إلى الاختبارات الشاملة end-to-end tests.
أهمية عملية الاختبار
نحن بشر والبشر يخطئون. الاختبار مهم لأنه يساعدك في الكشف عن هذه الأخطاء والتحقق من عمل شيفرتك، ويضمن الاختبار استمرار عمل شيفرتك في المستقبل عند إضافة ميزات جديدة، أو إعادة بناء الميزات الموجودة، أو ترقية الاعتماديات الرئيسية لمشروعك.
من أفضل الطرق لإصلاح خطأ في شيفرتك هي كتابة اختبار فاشل يكشف هذا الخطأ، ثم إن نجح هذا الاختبار عند إصلاح الخطأ وإعادة تشغيله، فهذا يعني أن الخطأ قد أُصلِح، ولن يُعاد إدخاله في قاعدة الشيفرة مطلقًا.
يمكن أيضًا أن تكون الاختبارات بمثابة توثيق للأشخاص الجدد الذين ينضمون إلى فريقك، إذ يمكن أن تساعد اختبارات القراءة الأشخاص الذين لم يروا قاعدة بيانات من قبل على فهم كيفية عمل الشيفرة الموجودة مسبقًا.
وأخيرًا، يؤدي إجراء المزيد من الاختبارات الآلية إلى قضاء وقت أقل في عملية ضمان الجودة اليدوية، مما يوفر وقتًا ثمينًا.
التحليل الساكن Static Analysis
تتمثل الخطوة الأولى لتحسين جودة الشيفرة في البدء في استخدام أدوات التحليل الساكنة، إذ يفحص التحليل الساكن شيفرتك بحثًا عن الأخطاء أثناء كتابتها دون تشغيل أي من تلك الشيفرة.
- تحلّل منقّحات الصياغة Linters الشيفرة للكشف عن الأخطاء الشائعة مثل الشيفرة غير المستخدَمة وللمساعدة في تجنب المخاطر، وللتنبيه بوجود دليلٍ من النمط المحرَّم مثل استخدام مفايتح tab بدلًا من المسافات (أو العكس اعتمادًا على الإعداد الخاص بك).
- يضمن التحقق من الأنواع Type checking تطابق البنية التي تمرّرها إلى دالة مع ما صُمِّمت الدالة لقبوله، كمنع تمرير سلسلة إلى دالة حسابية تقبل أعدادًا.
يأتي إطار عمل React Native مع أداتين من هذا القبيل هما: ESLint للكشف عن الأخطاء و Flow للتحقق من الأنواع. يمكنك أيضًا استخدام لغة TypeScript، وهي لغة مكتوبة مُصرَّفة إلى لغة JavaScript صِرفة.
كتابة شيفرة قابلة للاختبار
تحتاج لبدء الاختبارات أولًا إلى كتابة شيفرةٍ قابلة للاختبار. افترض مثلًا عملية تصنيع الطائرات، حيث يجري اختبار أجزاء الطائرة لضمان أنها آمنة وتعمل بصورةٍ صحيحة قبل إقلاع الطائرة لأول مرة لإثبات أن جميع أنظمتها المعقدة تعمل مع بعضها البعض بصورة جيدة، إذ تُختبَر الأجنحة عن طريق ثنيها تحت حمولة شديدة، وتُختبَر أجزاء المحرك للتأكد من متانتها، ويُختبَر الزجاج الأمامي للتأكد من قوته ضد تأثير محاكاة الطيور.
البرمجيات مشابهة للطائرات، إذ يمكنك كتابة شيفرتك ضمن وحدات صغيرة متعددة بدلًا من كتابة برنامجك بالكامل في ملف ضخم واحد يحتوي على العديد من الأسطر البرمجية، بحيث يمكنك اختبار وحدات الشيفرة اختبارًا أشمل من اختبار كل الشيفرة. تتشابك بهذه الطريقة كتابة الشيفرة القابلة للاختبار مع كتابة شيفرة معيارية دون اخطاء.
يمكنك جعل تطبيقك أكثر قابلية للاختبار من خلال فصل جزء عرض تطبيقك (أو مكونات React) عن منطق عملك وحالة التطبيق (بغض النظر عما إذا استخدمت Redux أو MobX أو حلولًا أخرى). يمكنك باستخدام هذه الطريقة الاحتفاظ باختبار منطق عملك (والذي لا ينبغي أن يعتمد على مكونات React) بصورةٍ مستقلة عن المكونات نفسها، فالهدف الأساسي هو تصيير rendering واجهة مستخدم تطبيقك.
يمكنك الذهاب إلى أبعد من ذلك نظريًا من خلال نقل كل المنطق والبيانات المجلوبة من مكوناتك، وبالتالي ستُخصَّص مكوناتك فقط لتصييرها، وستكون حالتك مستقلة تمامًا عن مكوناتك، إذ سيعمل منطق تطبيقك بدون أي مكونات React على الإطلاق.
اختبارات الكتابة
يجب الآن كتابة بعض الاختبارات الفعلية بعد كتابة شيفرة قابلة للاختبار. يُحمَّل قالب React Native الافتراضي مع إطار عمل اختبار Jest، ويشتمل هذا القالب على إعداد مسبق مصمَّم خصيصًا لهذه البيئة حتى تتمكن من زيادة الإنتاجية دون تعديل الإعداد وعمليات المحاكاة mocks (سنتكلم لاحقًا عن المحاكاة). يمكنك استخدام Jest لكتابة جميع أنواع الاختبارات.
ملاحظة: إن أجريت تطويرًا يعتمد على الاختبار، فيجب كتابة الاختبارات أولًا، وبالتالي تصبح شيفرتك قابلة للاختبار.
بنية الاختبارات
يجب أن تكون اختباراتك قصيرة وتختبر شيئًا واحدًا فقط. لنبدأ بمثال اختبار وحدة مكتوب باستخدام Jest:
it('given a date in the past, colorForDueDate() returns red', () => {
expect(colorForDueDate('2000-10-20')).toBe('red');
});
يوصَف الاختبار بواسطة السلسلة المُمرَّرة إلى الدالة it
. احرص على كتابة الوصف مع توضيح ما تختبره، إذ يجب أن يتضمن الوصف ما يلي:
- Given: بعض الشروط المسبقة.
- When: بعض الإجراءات التي تنفّذها الدالة التي تختبرها.
- Then: النتيجة المتوقعة.
يُعرف ذلك أيضًا باسم AAA (Arrange, Act, Assert).
يقدّم Jest دالة describe
للمساعدة في تنظيم اختباراتك. استخدم دالة describe
لتجميع جميع الاختبارات التي تنفّذ المهمة نفسها، ويمكن أن تتداخل الأوصاف، إذا كنت بحاجة إلى ذلك. الدوال الأخرى التي ستستخدمها هي beforeEach
أو beforeAll
التي يمكنك استخدامها لإعداد الكائنات التي تختبرها. اقرأ المزيد في مرجع Jest api reference.
إذا احتوى الاختبار الخاص بك على العديد من الخطوات أو التوقعات، فقد تقسّمه إلى عدة خطوات أصغر. تأكد أيضًا من أن اختباراتك مستقلة تمامًا عن بعضها البعض، إذ يجب أن يكون كل اختبار في مجموعتك قابلًا للتنفيذ بمفرده دون إجراء اختبار آخر أولًا. بينما إذا أجريت جميع اختباراتك معًا، فيجب ألا يؤثر الاختبار الأول على خرج الاختبار الثاني.
أخيرًا، يحب المطورون أن تعمل شيفرتهم دون أخطاء، والعكس صحيح عند تطبيق الاختبارات، فالاختبار الفاشل هو شيء جيد. يخبرنا فشل الاختبار أن هناك شيئًا ما ليس صحيحًا، وهذا يمنحك فرصة لإصلاح المشكلة قبل أن تؤثر على المستخدمين.
اختبارات الوحدة Unit tests
تغطي اختبارات الوحدة أصغر أجزاء الشيفرة، مثل الدوال أو الأصناف الفردية.
إذا احتوى الكائن قيد الاختبار على أي اعتماديات، فيجب محاكاته كما هو موضح في الفقرة التالية.
ميزة اختبارات الوحدة هي أنها سريعة في الكتابة والتشغيل، لذلك تحصل أثناء عملك على ملاحظات سريعة حول نجاح اختباراتك أو فشلها. يمتلك Jest أيضًا خيارًا لتشغيل الاختبارات ذات الصلة بالشيفرة التي تعدلها، وهذا الخيار هو الوضع Watch mode.
المحاكاة Mocking
قد ترغب أحيانًا في محاكاة الكائنات المُختبَرة عندما تحتوي على اعتماديات خارجية. "المحاكاة" Mocking هو استبدال جزء من اعتمادية شيفرتك بالتنفيذ الخاص بك بغرض التجريب والاختبار.
يُعَد استخدام أشياء حقيقية في اختباراتك أفضل من استخدام المحاكاة ولكن هناك مواقف لا يكون ذلك ممكنًا فيها، كالحالة التي يعتمد فيها اختبار وحدة JS على وحدة أصيلة مكتوبة بلغة Java أو Objective-C.
تخيل أنك تكتب تطبيقًا يعرض الطقس الحالي في مدينتك وتستخدم خدمة خارجية أو اعتمادية أخرى تزودك بمعلومات الطقس. إن أخبرتك الخدمة أن السماء تمطر، فستعرض صورة بها سحابة ممطرة. لا يجب استدعاء هذه الخدمة في اختباراتك للأسباب التالية:
- يمكن أن تبطّئ الاختبارات وتجعلها وغير مستقرة (بسبب طلبات الشبكة المشوَّشة).
- قد تعيد الخدمة بيانات مختلفة في كل مرة تشغّل فيها الاختبار.
- يمكن أن تصبح الخدمات الخارجية في وضع عدم الاتصال عندما تحتاج إلى إجراء الاختبارات.
لذلك يمكنك توفير تنفيذ خدمة محاكاة، واستبدال آلاف سطور الشيفرة وبعض الأجهزة المتصلة بالإنترنت بفعالية.
يأتي Jest مع دعم للمحاكاة ابتداءً من المحاكاة على مستوى الدالة وصولًا إلى المحاكاة على مستوى الوحدة.
اختبارات التكامل Integration Tests
يجب أن تتفاعل الأجزاء الفردية مع بعضها البعض عند كتابة أنظمة برمجية أكبر. إذا اعتمدت وحدتك على وحدة أخرى في اختبار الوحدة، فسينتهي بك الأمر في بعض الأحيان إلى محاكاة الاعتمادية، واستبدالها باعتمادية أخرى مزيفة.
تُدمَج الوحدات الفردية الحقيقية (كما هو الحال في تطبيقك) وتُختَبر معًا في اختبار التكامل للتأكد من أنها تتعاون كما هو متوقع. هذا لا يعني أن المحاكاة لا تحدث هنا، إذ ستظل بحاجة إلى المحاكاة (مثل محاكاة التواصل مع خدمة الطقس)، ولكنك ستحتاج إليه أقل بكثير من اختبار الوحدة.
لاحظ أن المصطلحات المتعلقة بما يعنيه اختبار التكامل ليست متناسقة دائمًا، وقد لا يكون الخط الفاصل بين اختبار الوحدة اختبار التكامل واضحًا دائمًا. يندرج اختبارك تحت النوع "اختبار التكامل" إذا كان:
- يجمع بين عدة وحدات من تطبيقك كما هو موضح أعلاه.
- يستخدم نظام خارجي.
- يستدعي تطبيقًا آخر عبر الشبكة (مثل واجهة برمجة تطبيقات خدمة الطقس).
- يجري أي نوع من أنواع إدخال وإخراج الملفات أو قواعد البيانات.
اختبار المكونات Component Tests
مكونات React هي المسؤولة عن تصيير تطبيقك، وسيتفاعل المستخدمون مباشرةً مع مخرجاتهم. حتى إن احتوى منطق عمل تطبيقك على تغطية اختبار عالية وكان صحيحًا، فلا يزال بإمكانك تقديم واجهة مستخدم معطلة للمستخدمين دون اختبار المكونات. يمكن أن تندرج اختبارات المكونات ضمن كل من اختبار الوحدة واختبار التكامل، ولكن سنشرحها بصورة منفصلة لأنها جزء أساسي من React Native.
هناك شيئان قد ترغب في اختبارهما لاختبار مكونات React هما:
- التفاعل Interaction: للتأكد من أن المكون يتصرّف تصرّفًا صحيحًا عند تفاعله مع المستخدم (عندما يضغط المستخدم على زر مثلًا).
- التصيير Rendering: للتأكد من صحة تصيير المكون الذي تستخدمه مكتبة React (كمظهر الزر وموضعه في واجهة المستخدم).
إذا كان لديك زر له مستمع onPress
، فيجب اختبار أن الزر يظهر بصورة صحيحة وأن النقر على الزر يعالجه المكون معالجةً صحيحة.
هناك العديد من المكتبات التي يمكن أن تساعدك في إجراء هذين الاختبارَين:
- يوفّر مصيّر الاختبار Test Renderer الخاص بمكتبة React، الذي طُوِّر جنبًا إلى جنب مع أساس React، مصيّر React يمكن استخدامه لتصيير مكونات React إلى كائنات JavaScript نقية، دون الاعتماد على DOM أو بيئة الجوّال الأصيلة.
- تُبنَى مكتبة React Native Testing Library على مصيّر اختبار React وتضيف
fireEvent
وquery
الخاص بواجهات برمجة التطبيقات الموضَّحة في الفقرة التالية.
اختبارات المكونات هي فقط اختبارات JavaScript مُشغَّلة في بيئة Node.js. لا تأخُذ هذه الاختبارات في الحسبان أي شيفرة لنظام iOS أو Android أو لأي منصة أخرى تدعم مكونات React Native، وبالتالي لا يمكن أن تمنحك هذه الاختبارات ثقة بنسبة 100% في أن كل شيء يعمل لدى المستخدم. إذا كان هناك خطأ في شيفرة iOS أو Android، فلن تعثر هذه الاختبارات عليه.
اختبار تفاعلات المستخدم
تتعامل مكوناتك -بغض النظر عن تصيير واجهة المستخدم- مع أحداث مثل onChangeText
للمكون TextInput
أو onPress
للمكوّن Button
، وقد تحتوي مكوناتك أيضًا على دوال أخرى وعمليات استدعاء للأحداث. افترض المثال التالي:
function GroceryShoppingList() {
const [groceryItem, setGroceryItem] = useState('');
const [items, setItems] = useState([]);
const addNewItemToShoppingList = useCallback(() => {
setItems([groceryItem, ...items]);
setGroceryItem('');
}, [groceryItem, items]);
return (
<>
<TextInput
value={groceryItem}
placeholder="Enter grocery item"
onChangeText={(text) => setGroceryItem(text)}
/>
<Button
title="Add the item to list"
onPress={addNewItemToShoppingList}
/>
{items.map((item) => (
<Text key={item}>{item}</Text>
))}
</>
);
}
اختبر المكون من منظور المستخدم عند اختبار تفاعلاته مثل اختبار ماذا يوجد في الصفحة؟ وما الذي يتغير عند التفاعل معها؟
يُفضَّل كقاعدة عامة استخدام الأشياء التي يمكن للمستخدمين رؤيتها أو سماعها مثل:
- عمل تأكيدات باستخدام النص المُصيَّر أو مساعدي إمكانية الوصول accessibility helpers
لكن يجب تجنّب ما يلي:
- إنشاء تأكيدات على خاصيات props المكون أو حالته state.
- استعلامات testID.
تجنّب اختبار تفاصيل التنفيذ مثل الخاصيات أو الحالة أثناء عمل هذه الاختبارات، فإنها ليست موجَّهة إلى كيفية تفاعل المستخدمين مع المكوّن وتميل إلى الانقطاع عن طريق إعادة البناء (عندما ترغب في إعادة تسمية بعض الأشياء أو إعادة كتابة مكون الصنف باستخدام الخطافات hooks على سبيل المثال).
تميل مكونات صنف React إلى اختبار تفاصيل التنفيذ الخاصة بها مثل الحالة الداخلية أو الخاصيات أو معالجات الأحداث. يفضَّل استخدام مكونات الدالة مع الخطافات لتجنب اختبار تفاصيل التنفيذ، مما يجعل الاعتماد على المكونات الداخلية أصعب.
تسهّل مكتبات اختبار المكونات مثل React Native Testing Library كتابة الاختبارات التي تركّز على المستخدم من خلال الاختيار الدقيق لواجهات برمجة التطبيقات المتوفرة. يستخدم المثال التالي توابع fireEvent
و changeText
و press
التي تحاكي مستخدمًا يتفاعل مع المكوّن ودالة استعلام getAllByText
التي تعثر على عُقد Text
المتطابقة في الخرج المُصيَّر.
test('given empty GroceryShoppingList, user can add an item to it', () => {
const { getByPlaceholder, getByText, getAllByText } = render(
<GroceryShoppingList />
);
fireEvent.changeText(
getByPlaceholder('Enter grocery item'),
'banana'
);
fireEvent.press(getByText('Add the item to list'));
const bananaElements = getAllByText('banana');
expect(bananaElements).toHaveLength(1); // expect 'banana' to be on the list
});
لا يختبر هذا المثال كيفية تغيّر بعض الحالات عند استدعاء دالة، ولكنه يختبر ما يحدث عندما يغير المستخدم النص في TextInput
وعندما يضغط على الزر Button
.
اختبار الخرج المُصيَّر (الناتج)
اختبار اللقطات (Snapshot testing) هو نوع متقدم من اختبارات Jest، وهو أداة قوية للغاية ومنخفضة المستوى، لذلك يُنصَح بالانتباه عند استخدامها.
"مكوّن اللقطة" (component snapshot) هو عبارة عن سلسلة تشبه JSX أنشأها مسلسِل React مخصَّص ومدمج في Jest. يتيح هذا المسلسِل لإطار Jest بترجمة أشجار مكونات React إلى سلسلة يمكن للبشر قراءتها، أي مكوّن اللقطة هو تمثيل نصي لخرج مصيّر مكوّنك الذي أُنشِئ أثناء تشغيل الاختبار، ويبدو كما يلي:
<Text
style={
Object {
"fontSize": 20,
"textAlign": "center",
}
}>
Welcome to React Native!
</Text>
تنفّذ باستخدام اختبار اللقطة أولًا مكوّنك ثم تشغّل اختبار اللقطة، ثم ينشئ اختبار اللقطة لقطةً ويحفظها في ملف في الريبو repo الخاص بك كلقطة مرجعية، ثم يجب الالتزام بالملف ويجري التحقق منه أثناء مراجعة الشيفرة. ستؤدي أي تغييرات مستقبلية على خرج مصيّر المكون إلى تغيير لقطته، مما يؤدي إلى فشل الاختبار. تحتاج بعد ذلك إلى تحديث اللقطة المرجعية المخزَّنة حتى ينجح الاختبار، ويجب الالتزام بهذا التغيير ومراجعته مرة أخرى.
تمتلك اللقطات نقاط ضعف متعددة وهي:
- قد يكون معرفة ما إذا كان التغيير في اللقطة مقصودًا أم أنه دليل على وجود خطأ أمرًا صعبًا بالنسبة لك كمطوّر أو كمراجِع. يمكن أن يصبح فهم اللقطات الكبيرة بشكل خاص أمرًا صعبًا وتصبح قيمتها المضافة منخفضة.
- تُعَد اللقطة صحيحة عند إنشائها، حتى في الحالة التي يكون فيها الخرج المُصيَّر خاطئًا بالفعل.
- يجب تحديث اللقطة عند فشلها باستخدام خيار jest الذي يُدعَى
--updateSnapshot
دون التحقق مما إذا كان التغيير متوقَّعًا أم لا، وبالتالي يجب على المطورين المحافظة على النظام.
لا تضمن اللقطات نفسها صحة منطق تصيير مكوّنك، فهي فقط جيدة في الحماية من التغييرات غير المتوقعة وللتحقق من أن المكونات في شجرة React قيد الاختبار تتلقى الخصائص المتوقعة (الأشكال وغير ذلك).
نوصي باستخدام لقطات صغيرة فقط (راجع قاعدة no-large-snapshots
). إن أردت اختبار تغيير بين حالتين من مكونات React، فاستخدم snapshot-diff
. يفضَّل استخدام التوقعات الصريحة كما هو موضح في الفقرة السابقة.
الاختبارات الشاملة End-to-End Tests
تتحقق في الاختبارات الشاملة E2E من أن تطبيقك يعمل كما هو متوقع على جهاز (أو محاكي / مقلِّد) من وجهة نظر المستخدم.
يمكن تنفيذ ذلك عن طريق بناء تطبيقك في إعداد الإصدار وتشغيل الاختبارات عليه. لن تفكر في اختبارات E2E بمكونات React أو واجهات برمجة تطبيقات React Native أو متاجر Redux أو أي منطق أعمال، إذ ليس هذا هو الغرض من اختبارات E2E والتي لا يمكن الوصول إليها حتى أثناء هذا اختبار.
تسمح لك مكتبات اختبار E2E بدلًا من ذلك بالعثور على العناصر والتحكم فيها في شاشة تطبيقك، حيث يمكنك بالفعل النقر على الأزرار أو إدراج نص في TextInputs
بنفس الطريقة التي يطبّقها المستخدم الحقيقي على سبيل المثال. يمكنك بعد ذلك إجراء تأكيدات حول ما إذا كان عنصر معين موجودًا في شاشة التطبيق أم لا، وما إذا كان مرئيًا أم لا، وما النص الذي يحتوي عليه، وما إلى ذلك.
تمنحك اختبارات E2E أعلى ثقة ممكنة بأن جزءًا من تطبيقك يعمل، ولكن:
- تستغرق كتابة هذه الاختبارات وقتًا أطول بالموازنة مع أنواع الاختبارات الأخرى.
- وهي أبطأ في التشغيل.
- وأكثر عرضة لعدم الاستقرار (الاختبار "غير المستقر flaky" هو اختبار ينجح ويفشل عشوائيًا دون أي تغيير في الشيفرة).
حاول تغطية الأجزاء الحيوية من تطبيقك باختبارات E2E مثل تدفق الاستيثاق والوظائف الأساسية والمدفوعات وما إلى ذلك، واستخدم اختبارات JS الأسرع للأجزاء غير الحيوية من تطبيقك. كلما أضفت المزيد من الاختبارات، كلما زادت ثقتك بنفسك، ولكن زاد أيضًا الوقت الذي تقضيه في صيانتها وتشغيلها، لذلك أجرِ حساباتك وحدّد الأفضل لك.
هناك العديد من أدوات اختبار E2E المتاحة، إذ يُعَد Detox إطار عمل شائعًا في مجتمع React Native لأنه مصمَّم لتطبيقات React Native. مكتبة أخرى شهيرة في فضاء تطبيقات iOS و Android هي Appium.
الخلاصة
هناك العديد من الطرق التي يمكنك من خلالها اختبار تطبيقاتك. قد يكون تحديد ما يجب استخدامه أمرًا صعبًا في البداية، ولكن نعتقد أن كل هذا سيكون منطقيًا بمجرد أن تبدأ في إضافة اختبارات إلى تطبيق React Native الخاص بك.