دليل كامل لاختبار خطافات التفاعل

نشرت: 2022-03-11

تم تقديم الخطافات في React 16.8 في أواخر عام 2018. وهي وظائف ترتبط بمكوِّن وظيفي وتسمح لنا باستخدام ميزات الحالة والمكون مثل componentDidUpdate و componentDidMount والمزيد. لم يكن هذا ممكنا من قبل.

أيضًا ، تسمح لنا الخطافات بإعادة استخدام منطق المكون والحالة عبر مكونات مختلفة. كان هذا صعبًا من قبل. لذلك ، كانت الخطافات عاملاً في تغيير قواعد اللعبة.

في هذه المقالة ، سوف نستكشف كيفية اختبار React Hooks. سنختار خطافًا معقدًا بدرجة كافية ونعمل على اختباره.

نتوقع أنك مطور متعطش لـ React وعلى دراية بالفعل بـ React Hooks. إذا كنت ترغب في زيادة معرفتك ، يجب عليك مراجعة البرنامج التعليمي الخاص بنا ، وإليك رابط التوثيق الرسمي.

الخطاف الذي سنستخدمه للاختبار

في هذه المقالة ، سنستخدم خطافًا كتبته في مقالتي السابقة ، Stale-while-revalidate Data Fetching with React Hooks. يسمى الخطاف useStaleRefresh . إذا لم تكن قد قرأت المقال ، فلا تقلق لأنني سألخص هذا الجزء هنا.

هذا هو الخطاف الذي سنختبره:

 import { useState, useEffect } from "react"; const CACHE = {}; export default function useStaleRefresh(url, defaultValue = []) { const [data, setData] = useState(defaultValue); const [isLoading, setLoading] = useState(true); useEffect(() => { // cacheID is how a cache is identified against a unique request const cacheID = url; // look in cache and set response if present if (CACHE[cacheID] !== undefined) { setData(CACHE[cacheID]); setLoading(false); } else { // else make sure loading set to true setLoading(true); setData(defaultValue); } // fetch new data fetch(url) .then((res) => res.json()) .then((newData) => { CACHE[cacheID] = newData; setData(newData); setLoading(false); }); }, [url, defaultValue]); return [data, isLoading]; }

كما ترى ، useStaleRefresh عبارة عن أداة ربط تساعد في جلب البيانات من عنوان URL أثناء إرجاع نسخة مخزنة مؤقتًا من البيانات ، إذا كانت موجودة. يستخدم متجرًا بسيطًا في الذاكرة للاحتفاظ بذاكرة التخزين المؤقت.

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

مخطط انسيابي يتتبع منطق التحديث الذي لا معنى له

في هذه المرحلة ، سأقترح عليك قضاء بعض الوقت في قراءة الخطاف أعلاه للحصول على فهم كامل لما يفعله.

في هذه المقالة ، سنرى كيف يمكننا اختبار هذا الخطاف ، أولاً باستخدام عدم وجود مكتبات اختبار (فقط أدوات اختبار React و Jest) ثم باستخدام مكتبة React-hooks-testing-library.

الدافع وراء عدم استخدام مكتبات اختبار ، أي مجرد عداء اختبار Jest ، هو توضيح كيفية عمل اختبار الخطاف. باستخدام هذه المعرفة ، ستتمكن من تصحيح أي مشكلات قد تنشأ عند استخدام مكتبة توفر اختبار التجريد.

تحديد حالات الاختبار

قبل أن نبدأ في اختبار هذا الخطاف ، دعنا نتوصل إلى خطة لما نريد اختباره. نظرًا لأننا نعرف ما يفترض أن يفعله الخطاف ، فإليك خطتي المكونة من ثماني خطوات لاختباره:

  1. عندما يتم ربط الخطاف بعنوان isLoading url1 true هي defaultValue .
  2. بعد طلب الجلب غير المتزامن ، يتم تحديث الخطاف ببيانات البيانات isLoading data1 false
  3. عندما يتم تغيير عنوان URL إلى url2 ، يصبح isLoading صحيحًا مرة أخرى وتكون البيانات هي defaultValue .
  4. بعد طلب الجلب غير المتزامن ، يتم تحديث الخطاف ببيانات بيانات جديدة data2 .
  5. بعد ذلك ، نقوم بتغيير عنوان URL مرة أخرى إلى url1 . يتم data1 البيانات 1 على الفور نظرًا لأنه يتم تخزينها مؤقتًا. isLoading خطأ.
  6. بعد طلب الجلب غير المتزامن ، عند تلقي استجابة جديدة ، يتم تحديث البيانات إلى data3 .
  7. بعد ذلك ، نقوم بتغيير عنوان URL مرة أخرى إلى url2 . يتم data2 على الفور حيث يتم تخزينها مؤقتًا. isLoading خطأ.
  8. بعد طلب الجلب غير المتزامن ، عند تلقي استجابة جديدة ، يتم تحديث البيانات إلى data4 .

يحدد تدفق الاختبار المذكور أعلاه بوضوح مسار كيفية عمل الخطاف. لذلك ، إذا تمكنا من ضمان نجاح هذا الاختبار ، فنحن جيدون.

تدفق الاختبار

اختبار الخطافات بدون مكتبة

في هذا القسم ، سنرى كيفية اختبار الخطافات دون استخدام أي مكتبات. سيوفر لنا هذا فهمًا عميقًا لكيفية اختبار خطافات React.

لبدء هذا الاختبار ، نود أن نسخر من عملية fetch . هذا حتى نتمكن من التحكم في ما تعيده واجهة برمجة التطبيقات. هنا هو fetch السخرية.

 function fetchMock(url, suffix = "") { return new Promise((resolve) => setTimeout(() => { resolve({ json: () => Promise.resolve({ data: url + suffix, }), }); }, 200 + Math.random() * 300) ); }

يفترض هذا fetch المعدل أن نوع الاستجابة هو دائمًا JSON ويعيد ، افتراضيًا ، url كقيمة data . كما أنه يضيف تأخيرًا عشوائيًا بين 200 مللي ثانية و 500 مللي ثانية للاستجابة.

إذا أردنا تغيير الاستجابة ، فنحن ببساطة suffix الوسيطة الثانية على قيمة سلسلة غير فارغة.

في هذه المرحلة قد تسأل لماذا التأخير؟ لماذا لا نرد الرد على الفور؟ هذا لأننا نريد تكرار العالم الحقيقي قدر الإمكان. لا يمكننا اختبار الخطاف بشكل صحيح إذا أعدناه على الفور. بالتأكيد ، يمكننا تقليل التأخير إلى 50-100 مللي ثانية لإجراء اختبارات أسرع ، لكن لا داعي للقلق بشأن ذلك في هذه المقالة.

بعد أن أصبح نموذج الجلب جاهزًا ، يمكننا ضبطه على وظيفة fetch . نحن نستخدم beforeAll و afterAll للقيام بذلك لأن هذه الوظيفة عديمة الحالة ، لذلك لا نحتاج إلى إعادة تعيينها بعد اختبار فردي.

 // runs before any tests start running beforeAll(() => { jest.spyOn(global, "fetch").mockImplementation(fetchMock); }); // runs after all tests have finished afterAll(() => { global.fetch.mockClear(); });

بعد ذلك ، نحتاج إلى تركيب الخطاف في أحد المكونات. لماذا ا؟ لأن الخطافات هي مجرد وظائف من تلقاء نفسها. فقط عند استخدامها في المكونات ، يمكنها الاستجابة إلى useState ، و useEffect ، وما إلى ذلك.

لذلك ، نحن بحاجة إلى إنشاء مكون اختبار يساعدنا على تثبيت TestComponent .

 // defaultValue is a global variable to avoid changing the object pointer on re-render // we can also deep compare `defaultValue` inside the hook's useEffect const defaultValue = { data: "" }; function TestComponent({ url }) { const [data, isLoading] = useStaleRefresh(url, defaultValue); if (isLoading) { return <div>loading</div>; } return <div>{data.data}</div>; }

هذا مكون بسيط يعرض البيانات أو يعرض مطالبة نصية "تحميل" إذا تم تحميل البيانات (يتم جلبها).

بمجرد أن نحصل على مكون الاختبار ، نحتاج إلى تثبيته على DOM. نستخدم beforeEach و afterEach لتركيب وإلغاء تحميل المكون الخاص بنا لكل اختبار لأننا نريد أن نبدأ بـ DOM جديد قبل كل اختبار.

 let container = null; beforeEach(() => { // set up a DOM element as a render target container = document.createElement("div"); document.body.appendChild(container); }); afterEach(() => { // cleanup on exiting unmountComponentAtNode(container); container.remove(); container = null; });

لاحظ أن container يجب أن تكون متغيرًا عامًا لأننا نريد الوصول إليها لتأكيدات الاختبار.

باستخدام هذه المجموعة ، دعنا نجري اختبارنا الأول حيث نعرض عنوان url1 URL ، وبما أن جلب عنوان URL سيستغرق بعض الوقت (انظر fetchMock ) ، يجب أن يعرض نص "التحميل" في البداية.

 it("useStaleRefresh hook runs correctly", () => { act(() => { render(<TestComponent url="url1" />, container); }); expect(container.textContent).toBe("loading"); })

قم بإجراء الاختبار باستخدام اختبار yarn test ، وسيعمل كما هو متوقع. هذا هو الكود الكامل على جيثب.

الآن ، دعنا نختبر متى يتغير نص loading هذا إلى بيانات الاستجابة التي تم جلبها ، url1 .

كيف نفعل ذلك؟ إذا نظرت إلى fetchMock ، فسترى أننا ننتظر 200-500 مللي ثانية. ماذا لو وضعنا sleep في الاختبار الذي ينتظر 500 مللي ثانية؟ سيغطي جميع أوقات الانتظار الممكنة. لنجرب ذلك.

 function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } it("useStaleRefresh hook runs correctly", async () => { act(() => { render(<TestComponent url="url1" />, container); }); expect(container.textContent).toBe("loading"); await sleep(500); expect(container.textContent).toBe("url1"); });

نجح الاختبار ، لكننا نرى خطأً أيضًا (رمز).

 PASS src/useStaleRefresh.test.js ✓ useStaleRefresh hook runs correctly (519ms) console.error node_modules/react-dom/cjs/react-dom.development.js:88 Warning: An update to TestComponent inside a test was not wrapped in act(...).

هذا لأن تحديث الحالة في الخطاف useStaleRefresh يحدث خارج الفعل (). للتأكد من معالجة تحديثات DOM في الوقت المناسب ، توصي React باستخدام act() في كل مرة قد يحدث فيها إعادة تصيير أو تحديث لواجهة المستخدم. لذلك ، نحن بحاجة إلى إنهاء نومنا act لأن هذا هو الوقت الذي يحدث فيه تحديث الحالة. بعد القيام بذلك ، يختفي الخطأ.

 import { act } from "react-dom/test-utils"; // ... await act(() => sleep(500));

الآن ، قم بتشغيله مرة أخرى (رمز على GitHub). كما هو متوقع ، فإنه يمر دون أخطاء.

دعنا نختبر الموقف التالي حيث قمنا أولاً بتغيير عنوان URL إلى url2 ، ثم تحقق من شاشة loading ، ثم انتظر استجابة الجلب ، وأخيراً تحقق من نص url2 . نظرًا لأننا نعرف الآن كيفية انتظار التغييرات غير المتزامنة بشكل صحيح ، يجب أن يكون ذلك سهلاً.

 act(() => { render(<TestComponent url="url2" />, container); }); expect(container.textContent).toContain("loading"); await act(() => sleep(500)); expect(container.textContent).toBe("url2");

قم بإجراء هذا الاختبار ، وسوف ينجح أيضًا. الآن ، يمكننا أيضًا اختبار الحالة التي تتغير فيها بيانات الاستجابة ويتم تشغيل ذاكرة التخزين المؤقت.

ستلاحظ أن لدينا suffix وسيطة إضافية في وظيفة fetchMock . هذا لتغيير بيانات الاستجابة. لذلك قمنا بتحديث نموذج الجلب الخاص بنا لاستخدام suffix .

 global.fetch.mockImplementation((url) => fetchMock(url, "__"));

الآن ، يمكننا اختبار الحالة حيث تم تعيين عنوان URL على url1 مرة أخرى. يقوم أولاً بتحميل url1 ثم url1__ . يمكننا أن نفعل الشيء نفسه مع url2 ، ويجب ألا تكون هناك مفاجآت.

 it("useStaleRefresh hook runs correctly", async () => { // ... // new response global.fetch.mockImplementation((url) => fetchMock(url, "__")); // set url to url1 again act(() => { render(<TestComponent url="url1" />, container); }); expect(container.textContent).toBe("url1"); await act(() => sleep(500)); expect(container.textContent).toBe("url1__"); // set url to url2 again act(() => { render(<TestComponent url="url2" />, container); }); expect(container.textContent).toBe("url2"); await act(() => sleep(500)); expect(container.textContent).toBe("url2__"); });

يمنحنا هذا الاختبار بالكامل الثقة في أن الخطاف يعمل بالفعل كما هو متوقع (رمز). يا هلا! الآن ، دعنا نلقي نظرة سريعة على التحسين باستخدام الطرق المساعدة.

تحسين الاختبار باستخدام الطرق المساعدة

حتى الآن ، رأينا كيفية اختبار الخطاف تمامًا. النهج ليس مثاليًا ولكنه يعمل. ومع ذلك ، هل يمكننا أن نفعل ما هو أفضل؟

نعم. لاحظ أننا ننتظر 500 مللي ثانية حتى تكتمل كل عملية جلب ، لكن كل طلب يستغرق أي شيء من 200 إلى 500 مللي ثانية. لذلك ، من الواضح أننا نضيع الوقت هنا. يمكننا التعامل مع هذا بشكل أفضل بمجرد انتظار الوقت الذي يستغرقه كل طلب.

كيف نفعل ذلك؟ أسلوب بسيط هو تنفيذ التأكيد حتى يمر أو يتم الوصول إلى مهلة. دعونا ننشئ وظيفة waitFor التي تقوم بذلك.

 async function waitFor(cb, timeout = 500) { const step = 10; let timeSpent = 0; let timedOut = false; while (true) { try { await sleep(step); timeSpent += step; cb(); break; } catch {} if (timeSpent >= timeout) { timedOut = true; break; } } if (timedOut) { throw new Error("timeout"); } }

تعمل هذه الوظيفة ببساطة على تشغيل رد اتصال (cb) داخل كتلة try...catch كل 10 مللي ثانية ، وإذا تم الوصول إلى timeout ، فستظهر خطأ. هذا يسمح لنا بتشغيل تأكيد حتى يمر بطريقة آمنة (أي ، لا توجد حلقات لانهائية).

يمكننا استخدامه في اختبارنا على النحو التالي: بدلاً من النوم لمدة 500 مللي ثانية ثم التأكيد ، نستخدم وظيفة waitFor بنا.

 // INSTEAD OF await act(() => sleep(500)); expect(container.textContent).toBe("url1"); // WE DO await act(() => waitFor(() => { expect(container.textContent).toBe("url1"); }) );

افعل ذلك في كل هذه التأكيدات ، ويمكننا أن نرى فرقًا كبيرًا في مدى سرعة تشغيل الاختبار (الكود).

الآن ، كل هذا رائع ، لكن ربما لا نريد اختبار الخطاف عبر واجهة المستخدم. ربما نريد اختبار خطاف باستخدام قيم العودة الخاصة به. كيف نفعل ذلك؟

لن يكون الأمر صعبًا لأننا بالفعل لدينا حق الوصول إلى قيم إرجاع الخطاف الخاص بنا. هم فقط داخل المكون. إذا تمكنا من إخراج هذه المتغيرات إلى النطاق العالمي ، فستنجح. لذلك دعونا نفعل ذلك.

نظرًا لأننا سنختبر الخطاف الخاص بنا من خلال القيمة المرتجعة وليس DOM ، يمكننا إزالة عرض HTML من المكون الخاص بنا وجعله null . يجب علينا أيضًا إزالة التدمير في عودة الخطاف لجعله أكثر عمومية. وبالتالي ، لدينا عنصر الاختبار المحدث هذا.

 // global variable let result; function TestComponent({ url }) { result = useStaleRefresh(url, defaultValue); return null; }

الآن يتم تخزين القيمة المرجعة للخطاف في result ، متغير عام. يمكننا الاستعلام عن تأكيداتنا.

 // INSTEAD OF expect(container.textContent).toContain("loading"); // WE DO expect(result[1]).toBe(true); // INSTEAD OF expect(container.textContent).toBe("url1"); // WE DO expect(result[0].data).toBe("url1");

بعد أن قمنا بتغييره في كل مكان ، يمكننا أن نرى أن اختباراتنا تجتاز (الكود).

في هذه المرحلة ، نحصل على جوهر اختبار React Hooks. هناك بعض التحسينات التي لا يزال بإمكاننا إجراؤها ، مثل:

  1. نقل متغير result إلى نطاق محلي
  2. إزالة الحاجة إلى إنشاء مكون لكل خطاف نريد اختباره

يمكننا القيام بذلك عن طريق إنشاء وظيفة مصنع بها مكون اختبار بداخلها. يجب أيضًا أن يعرض الخطاف في مكون الاختبار ويمنحنا الوصول إلى المتغير result . دعونا نرى كيف يمكننا القيام بذلك.

أولاً ، نقوم بنقل TestComponent result داخل الوظيفة. سنحتاج أيضًا إلى تمرير وسيطات Hook و Hook كوسيطات للوظيفة حتى يمكن استخدامها في مكون الاختبار الخاص بنا. باستخدام ذلك ، هذا ما لدينا. نحن نطلق على هذه الوظيفة renderHook .

 function renderHook(hook, args) { let result = {}; function TestComponent({ hookArgs }) { result.current = hook(...hookArgs); return null; } act(() => { render(<TestComponent hookArgs={args} />, container); }); return result; }

سبب حصولنا على result ككائن يخزن البيانات في result.current هو لأننا نريد تحديث قيم الإرجاع أثناء تشغيل الاختبار. القيمة المرجعة للخطاف الخاص بنا عبارة عن مصفوفة ، لذلك كان من الممكن نسخها بالقيمة إذا قمنا بإعادتها مباشرة. من خلال تخزينه في كائن ، نعيد مرجعًا لذلك الكائن بحيث يمكن تحديث قيم الإرجاع عن طريق تحديث result.current .

الآن ، كيف نبدأ في تحديث الخطاف؟ نظرًا لأننا نستخدم بالفعل الإغلاق ، rerender وظيفة أخرى يمكنه القيام بذلك.

تبدو وظيفة renderHook النهائية على النحو التالي:

 function renderHook(hook, args) { let result = {}; function TestComponent({ hookArgs }) { result.current = hook(...hookArgs); return null; } function rerender(args) { act(() => { render(<TestComponent hookArgs={args} />, container); }); } rerender(args); return { result, rerender }; }

الآن ، يمكننا استخدامه في اختبارنا. بدلًا من استخدام act render ، نقوم بما يلي:

 const { rerender, result } = renderHook(useStaleRefresh, [ "url1", defaultValue, ]);

بعد ذلك ، يمكننا التأكيد باستخدام result.current وتحديث الخطاف باستخدام rerender . إليك مثال بسيط:

 rerender(["url2", defaultValue]); expect(result.current[1]).toBe(true); // check isLoading is true

بمجرد تغييره في جميع الأماكن ، سترى أنه يعمل دون أي مشاكل (رمز).

متألق! الآن لدينا تجريد أكثر نظافة لاختبار الخطافات. لا يزال بإمكاننا القيام بعمل أفضل - على سبيل المثال ، يجب تمرير defaultValue في كل مرة لإعادة rerender على الرغم من أنها لا تتغير. يمكننا إصلاح ذلك.

لكن دعونا لا نتغلب على الأدغال كثيرًا لأن لدينا بالفعل مكتبة تعمل على تحسين هذه التجربة بشكل كبير.

أدخل مكتبة اختبار رد فعل الخطافات.

الاختبار باستخدام مكتبة React-hooks-testing-library

تقوم مكتبة React-hooks-Testing-library بكل ما تحدثنا عنه من قبل وبعد ذلك البعض. على سبيل المثال ، يتعامل مع تركيب الحاوية وإلغاء تركيبها حتى لا تضطر إلى القيام بذلك في ملف الاختبار الخاص بك. هذا يسمح لنا بالتركيز على اختبار الخطافات الخاصة بنا دون تشتيت الانتباه.

يأتي مع وظيفة renderHook التي ترجع rerender result . كما أنه يعيد wait ، وهو مشابه لـ waitFor ، لذلك لا يتعين عليك تنفيذه بنفسك.

إليك كيفية عرض خطاف في مكتبة React-hooks-testing-library. لاحظ أن الخطاف يتم تمريره في شكل رد اتصال. يتم تشغيل رد الاتصال هذا في كل مرة يتم فيها إعادة عرض مكون الاختبار.

 const { result, wait, rerender } = renderHook( ({ url }) => useStaleRefresh(url, defaultValue), { initialProps: { url: "url1", }, } );

بعد ذلك ، يمكننا اختبار ما إذا كان التصيير الأول نتج عنه isLoading كقيمة صحيحة وإرجاع القيمة defaultValue من خلال القيام بذلك. مشابه تمامًا لما طبقناه أعلاه.

 expect(result.current[0]).toEqual(defaultValue); expect(result.current[1]).toBe(true);

لاختبار التحديثات غير المتزامنة ، يمكننا استخدام طريقة wait التي renderHook . يأتي ملفوفًا بـ act() لذلك لا نحتاج إلى لف act() حوله.

 await wait(() => { expect(result.current[0].data).toEqual("url1"); }); expect(result.current[1]).toBe(false);

بعد ذلك ، يمكننا استخدام rerender جديدة. لاحظ أننا لا نحتاج إلى تمرير defaultValue هنا.

 rerender({ url: "url2" });

أخيرًا ، سيستمر باقي الاختبار بشكل مشابه (رمز).

تغليف

كان هدفي هو أن أوضح لك كيفية اختبار React Hooks من خلال أخذ مثال للخطاف غير المتزامن. آمل أن يساعدك هذا بثقة في التعامل مع اختبار أي نوع من الخطاف ، حيث يجب أن ينطبق نفس النهج على معظمهم.

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