الفرق بين المراجعتين لصفحة: «React/testing recipes»

من موسوعة حسوب
رفع المحتوى
 
ط مراجعة
سطر 2: سطر 2:
تحتوي هذه الصفحة على أكثر أنماط الاختبار شيوعًا لمكونات ريآكت.
تحتوي هذه الصفحة على أكثر أنماط الاختبار شيوعًا لمكونات ريآكت.


'''ملاحظة''': أُعدت هذه الصفحة بافتراض أنك تستخدم مكتبة [https://jestjs.io/ Jest] لإعداد بيئة الاختبار. إذا كنت تستخدم أداة اختبار أخرى فقد تحتاج إلى تعديل استعلامات واجهة برمجة التطبيق (API) لكن سيبقى الوصف العام لعملية الاختبار كما هو. راجع قسم إعداد بيئة الاختبار في صفحة بيئة الاختبار لمزيد من التفاصيل عن هذه النقطة.
'''ملاحظة''': أُعدّت هذه الصفحة بافتراض أنك تستخدم مكتبة [https://jestjs.io/ Jest] لإعداد بيئة الاختبار. إذا كنت تستخدم أداة اختبار أخرى فقد تحتاج إلى تعديل استعلامات واجهة برمجة التطبيق (API)، لكن سيبقى الوصف العام لعملية الاختبار كما هو. راجع قسم إعداد بيئة الاختبار في صفحة بيئة الاختبار لمزيد من التفاصيل عن هذه النقطة.


سنستخدم مكونات الدوال (function components) بشكل أساسي خلال هذه الصفحة لكن يجدر بك معرفة أن استراتيجيات الاختبار التالية لا تعتمد على تفاصيل التنفيذ إذ يمكن تطبيقها على مكونات الأصناف (class components) أيضًا.
سنستخدم مكوّنات الدوال (function components) أساسيًا خلال هذه الصفحة، لكن يجدر بك معرفة أنّ استراتيجيات الاختبار التالية لا تعتمد على تفاصيل التنفيذ، إذ يمكن تطبيقها على مكوِّنات الأصناف (class components) أيضًا.


محتويات الصفحة:
محتويات الصفحة:
سطر 14: سطر 14:
* مُحاكاة الوحدات (Mocking Modules)
* مُحاكاة الوحدات (Mocking Modules)
* الأحداث (Events)
* الأحداث (Events)
* المؤقتات (Timers)
* المؤقّْتات (Timers)
* اختبار التغيير (Snapshot Testing)
* اختبار التغيير (Snapshot Testing)
* التصيير المتعددة (Multiple Renderers)
* التصيير المتعددة (Multiple Renderers)
سطر 21: سطر 21:
== إعداد وإنهاء الاختبار ==
== إعداد وإنهاء الاختبار ==


ستحتاج عند إجراء كل اختبار إلى تصيير وعرض هيكل ريآكت الشجري وتحويله إلى عنصر نموذج كائن المستند (DOM) مُرتبط بصفحة التطبيق. تتمثل أهمية هذه النقطة في أنها خطوة أساسية تحتاجها عناصر الصفحة لاستقبال أحداث نموذج كائن المستند (DOM events). بعد ذلك سترغب في تنظيف مُخلفات الاختبار بمجرد انتهائه من الهيكل الشجري للصفحة.
ستحتاج عند إجراء كلّ اختبار إلى تصيير وعرض هيكل [[React|ريآكت]] الشجري وتحويله إلى عنصر نموذج كائن المستند (DOM) مُرتبط بصفحة التطبيق. تتمثل أهمية هذه النقطة في أنها تُعَدّ خطوة أساسية تحتاجها عناصر الصفحة لاستقبال أحداث نموذج كائن المستند (DOM events). بعد ذلك سترغب في تنظيف مُخلّفات الاختبار بمجرّد انتهائه من الهيكل الشجري للصفحة.


يُعد استخدام زوج من دوال <code>beforeEach</code> و <code>afterEach</code> أكثر الطرق شيوعًا لفعل ذلك إذ يمكن لتلك الطريقة تصيير وعزل تأثيرات الاختبار عن النسخة النهائية من الصفحة:
يُعَدّ استخدام زوج من دوال <code>beforeEach</code> و<code>afterEach</code> أكثر الطرق شيوعًا لفعل ذلك، إذ يمكن لتلك الطريقة تصيير وعزل تأثيرات الاختبار عن النسخة النهائية من الصفحة:


<syntaxhighlight class="">import { unmountComponentAtNode } from "react-dom";
<syntaxhighlight class="" lang="javascript">import { unmountComponentAtNode } from "react-dom";


let container = null;
let container = null;
سطر 41: سطر 41:
});
});
</syntaxhighlight>
</syntaxhighlight>
يمكنك أيضًا استخدام أنماط أخرى لكن تذكر أننا نريد تنفيذ عملية التنظيف حتى في حالة فشل الاختبار وإلا فقد يتسبب ذلك في حالات تسرّب عند تنفيذ اختبارات متعددة. يعني ذلك أن اختبارًا ما يمكنه التأثير على اختبار آخر ما يجعل تشخيص المشاكل وإصلاحها صعبًا.
يمكنك أيضًا استخدام أنماط أخرى، لكن تذكّر أننا نريد تنفيذ عملية التنظيف حتى في حالة فشل الاختبار وإلا فقد يتسبب ذلك في حالات تسرّب عند تنفيذ اختبارات متعدِّدة. يعني ذلك أنّ اختبارًا ما يمكنه التأثير على اختبار آخر ما يجعل تشخيص المشاكل وإصلاحها صعبًا.


== دالّة <code>act()</code> ==
== دالة <code>()act</code> ==


يمكنك اعتبار مهام مثل تصيير المكونات واختبار تفاعلات المستخدم وجلب البيانات كوحدات لتفاعل المستخدم مع التطبيق أثناء تصميمك اختبارات واجهة المستخدم. توفر أدوات react-dom/test-utils من ريآكت دالّة مُساعدة تُسمى <code>act()</code> تحرص على تصيير التحديثات المتعلقة بتلك العناصر وتطبيقها على نموذج كائن المستند قبل إجراء أي تقييم:
يمكنك احتساب مهام، مثل: تصيير المكونات، واختبار تفاعلات المستخدم، وجلب البيانات، وحداتٍ لتفاعل المستخدِم مع التطبيق أثناء تصميمك اختبارات واجهة المستخدم. توفِّر أدوات [[React/test utils|react-dom/test-utils]] من ريآكت دالّةً مُساعدةً تُسمى <code>()act</code> تحرص على تصيير التحديثات المتعلِّقة بتلك العناصر وتطبيقها على نموذج كائن المستند قبل إجراء أيّ تقييم:


<syntaxhighlight class="">act(() => {
<syntaxhighlight class="" lang="javascript">act(() => {
   // تصيير المكونات
   // تصيير المكونات
});
});
// تقييم النتائج</syntaxhighlight>
// تقييم النتائج</syntaxhighlight>
يساعد ذلك في جعل اختبارك يعمل بشكل مشابه لكيفية تفاعل المستخدم معه عند استخدام التطبيق. ستستخدم بقية الأمثلة التالية دالّة <code>act()</code> لاستغلال تلك النقطة.
يساعد ذلك في جعل اختبارك يعمل بما يشابه لكيفية تفاعل المستخدِم معه عند استخدام التطبيق. ستستعمل بقية الأمثلة التالية دالّة <code>()act</code> لاستغلال تلك النقطة.


من ناحية أخرى فقد تشعر بأن استخدام دالّة <code>act()</code> عملية مُضنية. لتفادي التكرار ننصح باستخدام مكتبات مثل مكتبة اختبار ريآكت [https://testing-library.com/react React Testing Library] إذ تستخدم مكوناتها دالة <code>act()</code> بشكل مُدمج.
من ناحية أخرى، فقد تشعر بأنّ استخدام دالّة <code>()act</code> عملية مُضنية. ولتفادي التكرار ننصح باستخدام مكتبات مثل مكتبة اختبار ريآكت [https://testing-library.com/react React Testing Library]، إذ تستخدم مكوناتها دالة <code>()act</code> بشكل مُدمج.


'''ملاحظة''': أتى اسم دالة <code>act()</code> من اسم نمط اختبار [http://wiki.c2.com/?ArrangeActAssert Arrange-Act-Assert].
'''ملاحظة''': أتى اسم دالة <code>()act</code> من اسم نمط اختبار [http://wiki.c2.com/?ArrangeActAssert Arrange-Act-Assert].


== التصيير ==
== التصيير ==


سترغب في العادة في اختبار إذا ما كانت المكونات تُعالج المعلومات المُدخلة بشكل صحيح. لنجرب اختبار مكون بسيط يعالج رسالة بناءً على مُدخلات معينة:
سترغب في العادة في اختبار إذا ما كانت المكوِّنات تُعالج المعلومات المُدخلة بطريقة صحيحو. لنجرب اختبار مكوِّن بسيط يعالج رسالة بناءً على مُدخلات معينة:


<syntaxhighlight class="">// hello.js
<syntaxhighlight class="" lang="javascript">// hello.js


import React from "react";
import React from "react";
سطر 72: سطر 72:
   }
   }
}</syntaxhighlight>
}</syntaxhighlight>
يمكننا تصميم اختبار لذلك المكون على النحو التالي:
يمكننا تصميم اختبار لذلك المكوِّن على النحو التالي:


<syntaxhighlight class="">// hello.test.js
<syntaxhighlight class="" lang="javascript">// hello.test.js


import React from "react";
import React from "react";
سطر 116: سطر 116:
يمكنك مُحاكاة الاستعلامات باستخدام بيانات مُزيفة عوضًا عن استخدام واجهة برمجة (API) حقيقية في اختباراتك. تساعد هذه الطريقة في منع انهيار الاختبارات بسبب عدم القدرة على الوصول إلى خادم البيانات البعيد كما تسرّع من تصيير هذه الاختبارات.
يمكنك مُحاكاة الاستعلامات باستخدام بيانات مُزيفة عوضًا عن استخدام واجهة برمجة (API) حقيقية في اختباراتك. تساعد هذه الطريقة في منع انهيار الاختبارات بسبب عدم القدرة على الوصول إلى خادم البيانات البعيد كما تسرّع من تصيير هذه الاختبارات.


'''ملاحظة''': قد ترغب أحيانًا في إجراء اختبارات كاملة فرعية باستخدام إطار عمل يدعم إنشاء مسارات اختبار كاملة (end-to-end) لتحديد إذا ما كان التطبيق كاملًا يعمل بشكل صحيح كوحدة واحدة.
'''ملاحظة''': قد ترغب أحيانًا في إجراء اختبارات كاملة فرعية باستخدام إطار عمل يدعم إنشاء مسارات اختبار كاملة (end-to-end)، لتحديد إذا ما كان التطبيق كاملًا يعمل بطريقة صحيحة مثل وحدة واحدة.


<syntaxhighlight class="">// user.js
<syntaxhighlight class="" lang="javascript">// user.js


import React, { useState, useEffect } from "react";
import React, { useState, useEffect } from "react";
سطر 147: سطر 147:
   );
   );
}</syntaxhighlight>
}</syntaxhighlight>
يمكننا اختبار المكون أعلاه على النحو التالي:
يمكننا اختبار المكوِّن أعلاه على النحو التالي:


<syntaxhighlight class="">// user.test.js
<syntaxhighlight class="" lang="javascript">// user.test.js


import React from "react";
import React from "react";
سطر 194: سطر 194:
   global.fetch.mockRestore();
   global.fetch.mockRestore();
});</syntaxhighlight>
});</syntaxhighlight>
== مُحاكاة الوحدات (Mocking Modules) ==
== محاكاة الوحدات (Mocking Modules) ==


قد لا تعمل بعض الوحدات بشكل جيد داخل بيئة الاختبار أو ليست ضرورية بالنسبة إلى الاختبار نفسه. لذا يُفضل مُحاكاة تلك الوحدات باستخدام بدائل مزيفة لتسهيل تصميم الاختبار الخاص بتطبيقك.
قد لا تعمل بعض الوحدات جيدًا داخل بيئة الاختبار، أو لا تُعَدّ ضروريةً بالنسبة إلى الاختبار نفسه. لذا يُفضّل مُحاكاة تلك الوحدات باستخدام بدائل مزيّفة لتسهيل تصميم الاختبار الخاص بتطبيقك.


لنفترض أن تطبيقك يتضمن مكوِّن جهات اتصال <code>Contact</code> يحتوي على ميزة حفظ الموقع باستخدام مكون داخلي مرتبط بخدمة خرائط جوجل <code>GoogleMap</code>:
لنفترض أنّ تطبيقك يتضمّن مكوِّن جهات اتصال <code>Contact</code>، ويحتوي على ميزة حفظ الموقع باستخدام مكون داخلي مرتبط بخدمة خرائط جوجل <code>GoogleMap</code>:


<syntaxhighlight class="">// map.js
<syntaxhighlight class="" lang="javascript">// map.js


import React from "react";
import React from "react";
سطر 234: سطر 234:
   );
   );
}</syntaxhighlight>
}</syntaxhighlight>
إذا لم تكن ترغب بتحميل هذا المكون ضمن اختبارك فيمكنك استبدال مُحاكاة ذلك الجزء عبر استخدام مكون مزيف وإجراء الاختبار:
إذا لم تكن ترغب بتحميل هذا المكوِّن ضمن اختبارك، فيمكنك استبدال مُحاكاة ذلك الجزء عبر استخدام مكوذِن مزيّف وإجراء الاختبار:


<syntaxhighlight class="">// contact.test.js
<syntaxhighlight class="" lang="javascript">// contact.test.js


import React from "react";
import React from "react";
سطر 297: سطر 297:
== الأحداث ==
== الأحداث ==


لكي تختبر تفاعل المستخدم مع صفحات تطبيقك يُوصى باستخدام أحداث حقيقة على عناصر نموذج كائن المستند (DOM) ثم تقييم تلك النتائج. على سبيل المثال ألقِ نظرة على مكوِّن زر التبديل (مكون باسم <code>Toggle</code>) التالي:
لكي تختبر تفاعل المستخدم مع صفحات تطبيقك يُوصى باستخدام أحداث حقيقة على عناصر نموذج كائن المستند (DOM) ثم تقييم تلك النتائج. ألقِ على سبيل المثال نظرةً على مكوِّن زرّ التبديل (مكون باسم <code>Toggle</code>) التالي:


<syntaxhighlight class="">// toggle.js
<syntaxhighlight class="" lang="javascript">// toggle.js


import React, { useState } from "react";
import React, { useState } from "react";
سطر 319: سطر 319:
يمكننا تصميم اختبار ملائم على النحو التالي:
يمكننا تصميم اختبار ملائم على النحو التالي:


<syntaxhighlight class="">// toggle.test.js
<syntaxhighlight class="" lang="javascript">// toggle.test.js


import React from "react";
import React from "react";
سطر 367: سطر 367:
   expect(button.innerHTML).toBe("Turn on");
   expect(button.innerHTML).toBe("Turn on");
});</syntaxhighlight>
});</syntaxhighlight>
يمكنك قراءة المزيد من التفاصيل عن أحداث نموذج كائن المستند المختلفة في توثيق شبكة مطوري موزيلا [https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent MDN]. أيضًا يجدر بنا تذكيركم بتمرير خاصية <code>{ bubbles: true }</code> لكل حدث تُنشئه لكي تتمكن ريآكت من رصده كونه يربط الأحداث تلقائيًا بجذر هيكل المستند.
يمكنك قراءة المزيد من التفاصيل عن أحداث نموذج كائن المستند المختلفة في توثيق شبكة مطوِّري موزيلا [https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent MDN]. أيضًا يجدر بنا تذكيركم بتمرير خاصية <code>{ bubbles: true }</code> لكل حدث تُنشئه لكي تتمكن ريآكت من رصده كونه يربط الأحداث تلقائيًا بجذر هيكل المستند.


'''ملاحظة''': توفر مكتبات اختبار ريآكت [https://testing-library.com/docs/dom-testing-library/api-events أدوات مساعدة مُختصرة] لتفعيل الأحداث المُرتبطة بالصفحة.
'''ملاحظة''': توفِّر مكتبات اختبار ريآكت [https://testing-library.com/docs/dom-testing-library/api-events أدوات مساعدة مُختصرة] لتفعيل الأحداث المُرتبطة بالصفحة.


== المؤقتات ==
== المؤقتات ==


قد يستخدم تطبيقك بعض الدوال المعتمدة على الوقت مثل دالة <code>setTimeout</code> لجدولة مهام معينة في المستقبل. يعرض المثال التالي لوحة تحتوي على خيارات متعددة تنتظر تحديد خيار ما قبل الانتقال إلى صفحة أخرى، وتمتلك مؤقتًا ينتهي خلال 5 ثوان إذا لم يحدد المستخدم أي خيار منها:
قد يستخدم تطبيقك بعض الدوال المعتمدة على الوقت مثل دالة <code>setTimeout</code> لجدولة مهام معيّنة في المستقبل. يعرض المثال التالي لوحة تحتوي على خيارات متعددة تنتظر تحديد خيار ما قبل الانتقال إلى صفحة أخرى، وتمتلك مؤقتًا ينتهي خلال 5 ثوان إذا لم يحدِّد المستخدم أيّ خيار منها:


<syntaxhighlight class="">// card.js
<syntaxhighlight class="" lang="javascript">// card.js


import React, { useEffect } from "react";
import React, { useEffect } from "react";
سطر 399: سطر 399:
   ));
   ));
}</syntaxhighlight>
}</syntaxhighlight>
يمكننا تصميم اختبارات هذا المكوّن عبر استخدام ميزة مُحاكاة [https://jestjs.io/docs/en/timer-mocks المؤقتات في مكتبة Jest] واختبار حالاته المتعددة كما في المثال التالي:
يمكننا تصميم اختبارات هذا المكوِّن عبر استخدام ميزة مُحاكاة [https://jestjs.io/docs/en/timer-mocks المؤقتات في مكتبة Jest] واختبار حالاته المتعددة كما في المثال التالي:


<syntaxhighlight class="">// card.test.js
<syntaxhighlight class="" lang="javascript">// card.test.js


import React from "react";
import React from "react";
سطر 478: سطر 478:
   expect(onSelect).toHaveBeenCalledWith(2);
   expect(onSelect).toHaveBeenCalledWith(2);
});</syntaxhighlight>
});</syntaxhighlight>
يمكنك استخدام مؤقتات مُزيفة في اختبارات معينة فقط، وكما هو ظاهر في المثال أعلاه فقد فعَّلنها عبر استخدام دالّة <code>jest.useFakeTimers()</code>. الهدف الرئيسي من فعل ذلك هو تفادي انتظار تلك الثواني الخمس قبل تنفيذ التعليمات البرمجية المُحددة كما لن تحتاج إلى كتابة تعليمات معقدة فقط لأجل الاختبار.
يمكنك استخدام مؤقِّتات مُزيفة في اختبارات معينة فقط، وكما هو ظاهر في المثال أعلاه فقد فعَّلنها عبر استخدام دالّة <code>()jest.useFakeTimers</code>. الهدف الرئيسي من فعل ذلك هو تفادي انتظار تلك الثواني الخمس قبل تنفيذ التعليمات البرمجية المُحددة كما لن تحتاج إلى كتابة تعليمات معقدة فقط لأجل الاختبار.


== اختبار التغيير ==
== اختبار التغيير ==


تُتيح لك أُطر عمل مثل Jest حفظ نسخة من بيانات تطبيقك عبر استخدام دوال <code>toMatchSnapshot</code> أو <code>toMatchInlineSnapshot</code>. هذا يمكننا حفظ نسخة مُصيّرة من مُخرجات المكوّن والتأكد من أن التغييرات المطبقة عليها توازي التغييرات التي أُجريت على النسخة الاحتياطية سابقة الذكر.
تُتيح لك أُطر عمل مثل Jest حفظ نسخة من بيانات تطبيقك عبر استخدام دوال <code>toMatchSnapshot</code> أو <code>toMatchInlineSnapshot</code>. هذا يمكننا من حفظ نسخة مُصيّرة من مُخرجات المكوّن والتأكد من موازاة التغييرات المطبقة عليها مع التغييرات التي أُجريت على النسخة الاحتياطية سابقة الذكر.


يوضح المثال التالي عملية تصيير مكون ما وتنسيق أكواد HTML المُنتجة باستخدام حزمة [https://www.npmjs.com/package/pretty pretty] قبل حفظها على هيئة كود مصدري:
يوضح المثال التالي عملية تصيير مكون ما وتنسيق أكواد [[HTML]] المُنتجة باستخدام حزمة [https://www.npmjs.com/package/pretty pretty] قبل حفظها على هيئة كود مصدري:


<syntaxhighlight class="">// hello.test.js, again
<syntaxhighlight class="" lang="javascript">// hello.test.js, again


import React from "react";
import React from "react";
سطر 534: سطر 534:
   ).toMatchInlineSnapshot(); /* ... ستملأ مكتبة jest محتويات هذا القسم تلقائيًا ... */
   ).toMatchInlineSnapshot(); /* ... ستملأ مكتبة jest محتويات هذا القسم تلقائيًا ... */
});</syntaxhighlight>
});</syntaxhighlight>
يستهدف إجراء الاختبارات عادةً التأكيد على نقطة معينة تُعد أكثر جدوى من مقارنة مُخرجات الاختبار. يُعزى السبب في ذلك إلى أن هذه الاختبارات تتضمن الكثير من تفاصيل التنفيذ ما يجعل الاختبار هشًا. لتفادي ذلك يمكنك مُحاكاة بعض المكونات المُتفرعة المُنتقاة لتقليل حجم النسخة التصيير وتسهيل مُراجعتها لاحقًا.
يستهدف إجراء الاختبارات عادةً التأكيد على نقطة معيّنة تُعَدّ أكثر جدوى من موازنة مُخرجات الاختبار. يُعزى السبب في ذلك إلى أنّ هذه الاختبارات تتضمّن الكثير من تفاصيل التنفيذ ما يجعل الاختبار هشًا. لتفادي ذلك يمكنك مُحاكاة بعض المكوِّنات المُتفرعة المُنتقاة لتقليل حجم النسخة التصيير وتسهيل مُراجعتها لاحقًا.


== التصيير المتعدد ==
== التصيير المتعدد ==


قد تحتاج أحيانًا إلى إجراء اختبار على مكوِّن يستخدم عدة خطوط تصيير. على سبيل المثال عند إجراء اختبار مقارن (snapshot test) على مكوِّن باستخدام حزمة تصيير react-test-renderer والتي تستخدم بدورها دالّة <code>ReactDOM.render</code> داخل مكوِّن فرعي لتصيير محتوى ما. في مثل هذه الحالات النادرة يمكنك إحاطة البيانات المُحدثة باستخدام دوال <code>act()</code> مرتبطة بكل عملية تصيير كما هو موضح أدناه:
قد تحتاج أحيانًا إلى إجراء اختبار على مكوِّن يستخدم عدة خطوط تصيير. على سبيل المثال عند إجراء اختبار التغيير (snapshot test) على مكوِّن باستخدام حزمة تصيير [[React/test renderer|react-test-renderer]] والتي تستخدم بدورها دالّة <code>ReactDOM.render</code> داخل مكوِّن فرعي لتصيير محتوى ما. في مثل هذه الحالات النادرة يمكنك إحاطة البيانات المُحدثة باستخدام دوال <code>()act</code> مرتبطة بكلّ عملية تصيير كما هو موضَّح أدناه:


<syntaxhighlight class="">import { act as domAct } from "react-dom/test-utils";
<syntaxhighlight class="" lang="javascript">import { act as domAct } from "react-dom/test-utils";
import { act as testAct, create } from "react-test-renderer";
import { act as testAct, create } from "react-test-renderer";
// ...
// ...

مراجعة 05:32، 26 مارس 2021

تحتوي هذه الصفحة على أكثر أنماط الاختبار شيوعًا لمكونات ريآكت.

ملاحظة: أُعدّت هذه الصفحة بافتراض أنك تستخدم مكتبة Jest لإعداد بيئة الاختبار. إذا كنت تستخدم أداة اختبار أخرى فقد تحتاج إلى تعديل استعلامات واجهة برمجة التطبيق (API)، لكن سيبقى الوصف العام لعملية الاختبار كما هو. راجع قسم إعداد بيئة الاختبار في صفحة بيئة الاختبار لمزيد من التفاصيل عن هذه النقطة.

سنستخدم مكوّنات الدوال (function components) أساسيًا خلال هذه الصفحة، لكن يجدر بك معرفة أنّ استراتيجيات الاختبار التالية لا تعتمد على تفاصيل التنفيذ، إذ يمكن تطبيقها على مكوِّنات الأصناف (class components) أيضًا.

محتويات الصفحة:

  • إعداد وإنهاء الاختبار
  • دالّة act()‎
  • التصيير (Rendering)
  • جلب البيانات (Data Fetching)
  • مُحاكاة الوحدات (Mocking Modules)
  • الأحداث (Events)
  • المؤقّْتات (Timers)
  • اختبار التغيير (Snapshot Testing)
  • التصيير المتعددة (Multiple Renderers)
  • سيناريو آخر؟

إعداد وإنهاء الاختبار

ستحتاج عند إجراء كلّ اختبار إلى تصيير وعرض هيكل ريآكت الشجري وتحويله إلى عنصر نموذج كائن المستند (DOM) مُرتبط بصفحة التطبيق. تتمثل أهمية هذه النقطة في أنها تُعَدّ خطوة أساسية تحتاجها عناصر الصفحة لاستقبال أحداث نموذج كائن المستند (DOM events). بعد ذلك سترغب في تنظيف مُخلّفات الاختبار بمجرّد انتهائه من الهيكل الشجري للصفحة.

يُعَدّ استخدام زوج من دوال beforeEach وafterEach أكثر الطرق شيوعًا لفعل ذلك، إذ يمكن لتلك الطريقة تصيير وعزل تأثيرات الاختبار عن النسخة النهائية من الصفحة:

import { unmountComponentAtNode } from "react-dom";

let container = null;
beforeEach(() => {
  // اختر عنصر DOM كهدف للتصيير
  container = document.createElement("div");
  document.body.appendChild(container);
});

afterEach(() => {
  // احذف المخلفات عند الخروج من الاختبار
  unmountComponentAtNode(container);
  container.remove();
  container = null;
});

يمكنك أيضًا استخدام أنماط أخرى، لكن تذكّر أننا نريد تنفيذ عملية التنظيف حتى في حالة فشل الاختبار وإلا فقد يتسبب ذلك في حالات تسرّب عند تنفيذ اختبارات متعدِّدة. يعني ذلك أنّ اختبارًا ما يمكنه التأثير على اختبار آخر ما يجعل تشخيص المشاكل وإصلاحها صعبًا.

دالة ()act

يمكنك احتساب مهام، مثل: تصيير المكونات، واختبار تفاعلات المستخدم، وجلب البيانات، وحداتٍ لتفاعل المستخدِم مع التطبيق أثناء تصميمك اختبارات واجهة المستخدم. توفِّر أدوات react-dom/test-utils من ريآكت دالّةً مُساعدةً تُسمى ()act تحرص على تصيير التحديثات المتعلِّقة بتلك العناصر وتطبيقها على نموذج كائن المستند قبل إجراء أيّ تقييم:

act(() => {
  // تصيير المكونات
});
// تقييم النتائج

يساعد ذلك في جعل اختبارك يعمل بما يشابه لكيفية تفاعل المستخدِم معه عند استخدام التطبيق. ستستعمل بقية الأمثلة التالية دالّة ()act لاستغلال تلك النقطة.

من ناحية أخرى، فقد تشعر بأنّ استخدام دالّة ()act عملية مُضنية. ولتفادي التكرار ننصح باستخدام مكتبات مثل مكتبة اختبار ريآكت React Testing Library، إذ تستخدم مكوناتها دالة ()act بشكل مُدمج.

ملاحظة: أتى اسم دالة ()act من اسم نمط اختبار Arrange-Act-Assert.

التصيير

سترغب في العادة في اختبار إذا ما كانت المكوِّنات تُعالج المعلومات المُدخلة بطريقة صحيحو. لنجرب اختبار مكوِّن بسيط يعالج رسالة بناءً على مُدخلات معينة:

// hello.js

import React from "react";

export default function Hello(props) {
  if (props.name) {
    return <h1>Hello, {props.name}!</h1>;
  } else {
    return <span>Hey, stranger</span>;
  }
}

يمكننا تصميم اختبار لذلك المكوِّن على النحو التالي:

// hello.test.js

import React from "react";
import { render, unmountComponentAtNode } from "react-dom";
import { act } from "react-dom/test-utils";

import Hello from "./hello";

let container = null;
beforeEach(() => {
  // اختر عنصر DOM كهدف للتصيير
  container = document.createElement("div");
  document.body.appendChild(container);
});

afterEach(() => {
  // احذف المخلفات عند الخروج من الاختبار
  unmountComponentAtNode(container);
  container.remove();
  container = null;
});

it("renders with or without a name", () => {
  act(() => {
    render(<Hello />, container);
  });
  expect(container.textContent).toBe("Hey, stranger");

  act(() => {
    render(<Hello name="Jenny" />, container);
  });
  expect(container.textContent).toBe("Hello, Jenny!");

  act(() => {
    render(<Hello name="Margaret" />, container);
  });
  expect(container.textContent).toBe("Hello, Margaret!");
});

جلب البيانات

يمكنك مُحاكاة الاستعلامات باستخدام بيانات مُزيفة عوضًا عن استخدام واجهة برمجة (API) حقيقية في اختباراتك. تساعد هذه الطريقة في منع انهيار الاختبارات بسبب عدم القدرة على الوصول إلى خادم البيانات البعيد كما تسرّع من تصيير هذه الاختبارات.

ملاحظة: قد ترغب أحيانًا في إجراء اختبارات كاملة فرعية باستخدام إطار عمل يدعم إنشاء مسارات اختبار كاملة (end-to-end)، لتحديد إذا ما كان التطبيق كاملًا يعمل بطريقة صحيحة مثل وحدة واحدة.

// user.js

import React, { useState, useEffect } from "react";

export default function User(props) {
  const [user, setUser] = useState(null);

  async function fetchUserData(id) {
    const response = await fetch("/" + id);
    setUser(await response.json());
  }

  useEffect(() => {
    fetchUserData(props.id);
  }, [props.id]);

  if (!user) {
    return "loading...";
  }

  return (
    <details>
      <summary>{user.name}</summary>
      <strong>{user.age}</strong> years old
      <br />
      lives in {user.address}
    </details>
  );
}

يمكننا اختبار المكوِّن أعلاه على النحو التالي:

// user.test.js

import React from "react";
import { render, unmountComponentAtNode } from "react-dom";
import { act } from "react-dom/test-utils";
import User from "./user";

let container = null;
beforeEach(() => {
  // اختر عنصر DOM كهدف للتصيير
  container = document.createElement("div");
  document.body.appendChild(container);
});

afterEach(() => {
  // احذف المخلفات عند الخروج من الاختبار
  unmountComponentAtNode(container);
  container.remove();
  container = null;
});

it("renders user data", async () => {
  const fakeUser = {
    name: "Joni Baez",
    age: "32",
    address: "123, Charming Avenue"
  };
  jest.spyOn(global, "fetch").mockImplementation(() =>
    Promise.resolve({
      json: () => Promise.resolve(fakeUser)
    })
  );

  // استخدم نسخة غير متزامنة من دالّة act لتطبيق ناتج العملية المُنفذة (promise)
  await act(async () => {
    render(<User id="123" />, container);
  });

  expect(container.querySelector("summary").textContent).toBe(fakeUser.name);
  expect(container.querySelector("strong").textContent).toBe(fakeUser.age);
  expect(container.textContent).toContain(fakeUser.address);

  // احذف عملية المُحاكاة للتأكد من بقاء الاختبار معزولًا
  global.fetch.mockRestore();
});

محاكاة الوحدات (Mocking Modules)

قد لا تعمل بعض الوحدات جيدًا داخل بيئة الاختبار، أو لا تُعَدّ ضروريةً بالنسبة إلى الاختبار نفسه. لذا يُفضّل مُحاكاة تلك الوحدات باستخدام بدائل مزيّفة لتسهيل تصميم الاختبار الخاص بتطبيقك.

لنفترض أنّ تطبيقك يتضمّن مكوِّن جهات اتصال Contact، ويحتوي على ميزة حفظ الموقع باستخدام مكون داخلي مرتبط بخدمة خرائط جوجل GoogleMap:

// map.js

import React from "react";

import { LoadScript, GoogleMap } from "react-google-maps";
export default function Map(props) {
  return (
    <LoadScript id="script-loader" googleMapsApiKey="YOUR_API_KEY">
      <GoogleMap id="example-map" center={props.center} />
    </LoadScript>
  );
}

// contact.js

import React from "react";
import Map from "./map";

export default function Contact(props) {
  return (
    <div>
      <address>
        Contact {props.name} via{" "}
        <a data-testid="email" href={"mailto:" + props.email}>
          email
        </a>
        or on their <a data-testid="site" href={props.site}>
          website
        </a>.
      </address>
      <Map center={props.center} />
    </div>
  );
}

إذا لم تكن ترغب بتحميل هذا المكوِّن ضمن اختبارك، فيمكنك استبدال مُحاكاة ذلك الجزء عبر استخدام مكوذِن مزيّف وإجراء الاختبار:

// contact.test.js

import React from "react";
import { render, unmountComponentAtNode } from "react-dom";
import { act } from "react-dom/test-utils";

import Contact from "./contact";
import MockedMap from "./map";

jest.mock("./map", () => {
  return function DummyMap(props) {
    return (
      <div data-testid="map">
        {props.center.lat}:{props.center.long}
      </div>
    );
  };
});

let container = null;
beforeEach(() => {
  // اختر عنصر DOM كهدف للتصيير
  container = document.createElement("div");
  document.body.appendChild(container);
});

afterEach(() => {
  // احذف المخلفات عند الخروج من الاختبار
  unmountComponentAtNode(container);
  container.remove();
  container = null;
});

it("should render contact information", () => {
  const center = { lat: 0, long: 0 };
  act(() => {
    render(
      <Contact
        name="Joni Baez"
        email="test@example.com"
        site="http://test.com"
        center={center}
      />,
      container
    );
  });

  expect(
    container.querySelector("[data-testid='email']").getAttribute("href")
  ).toEqual("mailto:test@example.com");

  expect(
    container.querySelector('[data-testid="site"]').getAttribute("href")
  ).toEqual("http://test.com");

  expect(container.querySelector('[data-testid="map"]').textContent).toEqual(
    "0:0"
  );
});

الأحداث

لكي تختبر تفاعل المستخدم مع صفحات تطبيقك يُوصى باستخدام أحداث حقيقة على عناصر نموذج كائن المستند (DOM) ثم تقييم تلك النتائج. ألقِ على سبيل المثال نظرةً على مكوِّن زرّ التبديل (مكون باسم Toggle) التالي:

// toggle.js

import React, { useState } from "react";

export default function Toggle(props) {
  const [state, setState] = useState(false);
  return (
    <button
      onClick={() => {
        setState(previousState => !previousState);
        props.onChange(!state);
      }}
      data-testid="toggle"
    >
      {state === true ? "Turn off" : "Turn on"}
    </button>
  );
}

يمكننا تصميم اختبار ملائم على النحو التالي:

// toggle.test.js

import React from "react";
import { render, unmountComponentAtNode } from "react-dom";
import { act } from "react-dom/test-utils";

import Toggle from "./toggle";

let container = null;
beforeEach(() => {
  // اختر عنصر DOM كهدف للتصيير
  container = document.createElement("div");
  document.body.appendChild(container);
});

afterEach(() => {
  // احذف المخلفات عند الخروج من الاختبار
  unmountComponentAtNode(container);
  container.remove();
  container = null;
});

it("changes value when clicked", () => {
  const onChange = jest.fn();
  act(() => {
    render(<Toggle onChange={onChange} />, container);
  });

  // استهدف الزر المُحدد وحاكي عملية النقر عليه
  const button = document.querySelector("[data-testid=toggle]");
  expect(button.innerHTML).toBe("Turn on");

  act(() => {
    button.dispatchEvent(new MouseEvent("click", { bubbles: true }));
  });

  expect(onChange).toHaveBeenCalledTimes(1);
  expect(button.innerHTML).toBe("Turn off");

  act(() => {
    for (let i = 0; i < 5; i++) {
      button.dispatchEvent(new MouseEvent("click", { bubbles: true }));
    }
  });

  expect(onChange).toHaveBeenCalledTimes(6);
  expect(button.innerHTML).toBe("Turn on");
});

يمكنك قراءة المزيد من التفاصيل عن أحداث نموذج كائن المستند المختلفة في توثيق شبكة مطوِّري موزيلا MDN. أيضًا يجدر بنا تذكيركم بتمرير خاصية { bubbles: true } لكل حدث تُنشئه لكي تتمكن ريآكت من رصده كونه يربط الأحداث تلقائيًا بجذر هيكل المستند.

ملاحظة: توفِّر مكتبات اختبار ريآكت أدوات مساعدة مُختصرة لتفعيل الأحداث المُرتبطة بالصفحة.

المؤقتات

قد يستخدم تطبيقك بعض الدوال المعتمدة على الوقت مثل دالة setTimeout لجدولة مهام معيّنة في المستقبل. يعرض المثال التالي لوحة تحتوي على خيارات متعددة تنتظر تحديد خيار ما قبل الانتقال إلى صفحة أخرى، وتمتلك مؤقتًا ينتهي خلال 5 ثوان إذا لم يحدِّد المستخدم أيّ خيار منها:

// card.js

import React, { useEffect } from "react";

export default function Card(props) {
  useEffect(() => {
    const timeoutID = setTimeout(() => {
      props.onSelect(null);
    }, 5000);
    return () => {
      clearTimeout(timeoutID);
    };
  }, [props.onSelect]);

  return [1, 2, 3, 4].map(choice => (
    <button
      key={choice}
      data-testid={choice}
      onClick={() => props.onSelect(choice)}
    >
      {choice}
    </button>
  ));
}

يمكننا تصميم اختبارات هذا المكوِّن عبر استخدام ميزة مُحاكاة المؤقتات في مكتبة Jest واختبار حالاته المتعددة كما في المثال التالي:

// card.test.js

import React from "react";
import { render, unmountComponentAtNode } from "react-dom";
import { act } from "react-dom/test-utils";

import Card from "./card";

jest.useFakeTimers();

let container = null;
beforeEach(() => {
  // اختر عنصر DOM كهدف للتصيير
  container = document.createElement("div");
  document.body.appendChild(container);
});

afterEach(() => {
  // احذف المخلفات عند الخروج من الاختبار
  unmountComponentAtNode(container);
  container.remove();
  container = null;
});

it("should select null after timing out", () => {
  const onSelect = jest.fn();
  act(() => {
    render(<Card onSelect={onSelect} />, container);
  });

  // قدِّم الوقت بمقدار 100 مللي ثانية
  act(() => {
    jest.advanceTimersByTime(100);
  });
  expect(onSelect).not.toHaveBeenCalled();

  // قدِّم الوقت مجددًا بمقدار 5 ثوان
  act(() => {
    jest.advanceTimersByTime(5000);
  });
  expect(onSelect).toHaveBeenCalledWith(null);
});

it("should cleanup on being removed", () => {
  const onSelect = jest.fn();
  act(() => {
    render(<Card onSelect={onSelect} />, container);
  });
  act(() => {
    jest.advanceTimersByTime(100);
  });
  expect(onSelect).not.toHaveBeenCalled();

  // unmount the app
  act(() => {
    render(null, container);
  });
  act(() => {
    jest.advanceTimersByTime(5000);
  });
  expect(onSelect).not.toHaveBeenCalled();
});

it("should accept selections", () => {
  const onSelect = jest.fn();
  act(() => {
    render(<Card onSelect={onSelect} />, container);
  });

  act(() => {
    container
      .querySelector("[data-testid='2']")
      .dispatchEvent(new MouseEvent("click", { bubbles: true }));
  });

  expect(onSelect).toHaveBeenCalledWith(2);
});

يمكنك استخدام مؤقِّتات مُزيفة في اختبارات معينة فقط، وكما هو ظاهر في المثال أعلاه فقد فعَّلنها عبر استخدام دالّة ()jest.useFakeTimers. الهدف الرئيسي من فعل ذلك هو تفادي انتظار تلك الثواني الخمس قبل تنفيذ التعليمات البرمجية المُحددة كما لن تحتاج إلى كتابة تعليمات معقدة فقط لأجل الاختبار.

اختبار التغيير

تُتيح لك أُطر عمل مثل Jest حفظ نسخة من بيانات تطبيقك عبر استخدام دوال toMatchSnapshot أو toMatchInlineSnapshot. هذا يمكننا من حفظ نسخة مُصيّرة من مُخرجات المكوّن والتأكد من موازاة التغييرات المطبقة عليها مع التغييرات التي أُجريت على النسخة الاحتياطية سابقة الذكر.

يوضح المثال التالي عملية تصيير مكون ما وتنسيق أكواد HTML المُنتجة باستخدام حزمة pretty قبل حفظها على هيئة كود مصدري:

// hello.test.js, again

import React from "react";
import { render, unmountComponentAtNode } from "react-dom";
import { act } from "react-dom/test-utils";
import pretty from "pretty";

import Hello from "./hello";

let container = null;
beforeEach(() => {
  // اختر عنصر DOM كهدف للتصيير
  container = document.createElement("div");
  document.body.appendChild(container);
});

afterEach(() => {
  // احذف المخلفات عند الخروج من الاختبار
  unmountComponentAtNode(container);
  container.remove();
  container = null;
});

it("should render a greeting", () => {
  act(() => {
    render(<Hello />, container);
  });

  expect(
    pretty(container.innerHTML)
  ).toMatchInlineSnapshot(); /* ... ستملأ مكتبة jest محتويات هذا القسم تلقائيًا ... */

  act(() => {
    render(<Hello name="Jenny" />, container);
  });

  expect(
    pretty(container.innerHTML)
  ).toMatchInlineSnapshot(); /* ... ستملأ مكتبة jest محتويات هذا القسم تلقائيًا ... */

  act(() => {
    render(<Hello name="Margaret" />, container);
  });

  expect(
    pretty(container.innerHTML)
  ).toMatchInlineSnapshot(); /* ... ستملأ مكتبة jest محتويات هذا القسم تلقائيًا ... */
});

يستهدف إجراء الاختبارات عادةً التأكيد على نقطة معيّنة تُعَدّ أكثر جدوى من موازنة مُخرجات الاختبار. يُعزى السبب في ذلك إلى أنّ هذه الاختبارات تتضمّن الكثير من تفاصيل التنفيذ ما يجعل الاختبار هشًا. لتفادي ذلك يمكنك مُحاكاة بعض المكوِّنات المُتفرعة المُنتقاة لتقليل حجم النسخة التصيير وتسهيل مُراجعتها لاحقًا.

التصيير المتعدد

قد تحتاج أحيانًا إلى إجراء اختبار على مكوِّن يستخدم عدة خطوط تصيير. على سبيل المثال عند إجراء اختبار التغيير (snapshot test) على مكوِّن باستخدام حزمة تصيير react-test-renderer والتي تستخدم بدورها دالّة ReactDOM.render داخل مكوِّن فرعي لتصيير محتوى ما. في مثل هذه الحالات النادرة يمكنك إحاطة البيانات المُحدثة باستخدام دوال ()act مرتبطة بكلّ عملية تصيير كما هو موضَّح أدناه:

import { act as domAct } from "react-dom/test-utils";
import { act as testAct, create } from "react-test-renderer";
// ...
let root;
domAct(() => {
  testAct(() => {
    root = create(<App />);
  });
});
expect(root).toMatchSnapshot();

حالات شائعة أخرى

إذا وُجدت حالات أخرى لم تُذكر في هذه الصفحة فنرجو منك إعلامنا أو الإشارة إلى ذلك في القسم المخصص بتوثيق ريآكت.

ترجمة -وبتصرف- للمقال Testing Recipes، من توثيق ريآكت