زيادة قابلية الحفاظ على التعليمات البرمجية مع اختبار تكامل التفاعل
نشرت: 2022-03-11تعتبر اختبارات التكامل نقطة جيدة بين تكلفة الاختبارات وقيمتها. يمكن أن تؤدي كتابة اختبارات التكامل لتطبيق React بمساعدة مكتبة اختبار التفاعل بدلاً من اختبارات وحدة المكونات أو بالإضافة إليها إلى زيادة قابلية صيانة الكود دون الإضرار بسرعة التطوير.
في حالة رغبتك في الحصول على السبق قبل المتابعة ، يمكنك الاطلاع على مثال على كيفية استخدام مكتبة اختبار التفاعل لاختبارات تكامل تطبيق React هنا.
لماذا الاستثمار في اختبار التكامل؟
"تحقق اختبارات التكامل توازنًا كبيرًا في المفاضلات بين الثقة والسرعة / المصروفات. ولهذا السبب يُنصح بإنفاق معظم (وليس كل ما عليك فعله) من جهدك هناك."
- كينت سي دودس في اختبارات الكتابة. ليس بالكثير. في الغالب التكامل.
من الشائع كتابة اختبارات الوحدة لمكونات React ، وغالبًا ما تستخدم مكتبة شائعة لاختبار "إنزيمات" React ؛ على وجه التحديد ، طريقته "الضحلة". يتيح لنا هذا الأسلوب اختبار المكونات بمعزل عن باقي التطبيق. ومع ذلك ، نظرًا لأن كتابة تطبيقات React تدور حول تكوين المكونات ، فإن اختبارات الوحدة وحدها لا تضمن أن التطبيق خالي من الأخطاء.
على سبيل المثال ، قد يؤدي تغيير الخاصيات المقبولة للمكون وتحديث اختبارات الوحدة المرتبطة به إلى اجتياز جميع الاختبارات بينما قد يظل التطبيق معطلاً إذا لم يتم تحديث مكون آخر وفقًا لذلك.
يمكن أن تساعد اختبارات التكامل في الحفاظ على راحة البال أثناء إجراء تغييرات على تطبيق React ، لأنها تضمن أن تكوين المكونات ينتج عنه تجربة المستخدم المطلوبة.
متطلبات اختبارات تكامل تطبيق React
فيما يلي بعض الأشياء التي يريد مطورو React القيام بها عند كتابة اختبارات التكامل:
- اختبار حالات استخدام التطبيق من وجهة نظر المستخدم. يصل المستخدمون إلى المعلومات الموجودة على صفحة الويب ويتفاعلون مع عناصر التحكم المتاحة.
- مكالمات API الوهمية لا تعتمد على توفر API وحالة اجتياز / فشل الاختبارات.
- واجهات برمجة تطبيقات متصفح وهمية (على سبيل المثال ، التخزين المحلي) لأنها ببساطة غير موجودة في بيئة الاختبار.
- التأكيد على حالة DOM من React (متصفح DOM أو بيئة محمولة أصلية).
الآن ، بالنسبة لبعض الأشياء التي يجب أن نحاول تجنبها عند كتابة اختبارات تكامل تطبيق React:
- تفاصيل تنفيذ الاختبار. يجب ألا تؤدي تغييرات التنفيذ إلى الاختبار إلا إذا كانت قد أدخلت خطأ بالفعل.
- تسخر كثيرا. نريد اختبار كيفية عمل جميع أجزاء التطبيق معًا.
- تصيير ضحلة. نريد اختبار تكوين جميع المكونات في التطبيق وصولاً إلى أصغر مكون.
لماذا تختار مكتبة اختبار React؟
تجعل المتطلبات المذكورة أعلاه مكتبة اختبار التفاعل خيارًا رائعًا ، حيث أن مبدأها التوجيهي الرئيسي هو السماح باختبار مكونات React بطريقة تشبه كيفية استخدامها من قبل الإنسان الفعلي.
تتيح لنا المكتبة ، جنبًا إلى جنب مع المكتبات المصاحبة الاختيارية ، كتابة الاختبارات التي تتفاعل مع DOM والتأكيد على حالتها.
نموذج إعداد التطبيق
التطبيق الذي سنكتب له عينة من اختبارات التكامل ينفذ سيناريو بسيطًا:
- يقوم المستخدم بإدخال اسم مستخدم GitHub.
- يعرض التطبيق قائمة المستودعات العامة المرتبطة باسم المستخدم الذي تم إدخاله.
يجب أن تكون كيفية تنفيذ الوظيفة المذكورة أعلاه غير ذات صلة من منظور اختبار التكامل. ومع ذلك ، للبقاء بالقرب من تطبيقات العالم الحقيقي ، يتبع التطبيق أنماط React الشائعة ، ومن هنا يأتي التطبيق:
- هو تطبيق من صفحة واحدة (SPA).
- يجعل طلبات API.
- لديها إدارة الدولة العالمية.
- يدعم التدويل.
- يستخدم مكتبة مكونات React.
يمكن العثور على الكود المصدري لتطبيق التطبيق هنا.
كتابة اختبارات التكامل
تثبيت التبعيات
مع الغزل:
yarn add --dev jest @testing-library/react @testing-library/user-event jest-dom nock
أو مع npm:
npm i -D jest @testing-library/react @testing-library/user-event jest-dom nock
إنشاء ملف مجموعة اختبار التكامل
سننشئ ملفًا باسم viewGitHubRepositoriesByUsername.spec.js
ملف في مجلد ./test
. سوف تلتقطه Jest تلقائيًا.
استيراد التبعيات في ملف الاختبار
import React from 'react'; // so that we can use JSX syntax import { render, cleanup, waitForElement } from '@testing-library/react'; // testing helpers import userEvent from '@testing-library/user-event' // testing helpers for imitating user events import 'jest-dom/extend-expect'; // to extend Jest's expect with DOM assertions import nock from 'nock'; // to mock github API import { FAKE_USERNAME_WITH_REPOS, FAKE_USERNAME_WITHOUT_REPOS, FAKE_BAD_USERNAME, REPOS_LIST } from './fixtures/github'; // test data to use in a mock API import './helpers/initTestLocalization'; // to configure i18n for tests import App from '../App'; // the app that we are going to test
إعداد مجموعة الاختبار
describe('view GitHub repositories by username', () => { beforeAll(() => { nock('https://api.github.com') .persist() .get(`/users/${FAKE_USERNAME_WITH_REPOS}/repos`) .query(true) .reply(200, REPOS_LIST); }); afterEach(cleanup); describe('when GitHub user has public repositories', () => { it('user can view the list of public repositories for entered GitHub username', async () => { // arrange // act // assert }); }); describe('when GitHub user has no public repositories', () => { it('user is presented with a message that there are no public repositories for entered GitHub username', async () => { // arrange // act // assert }); }); describe('when GitHub user does not exist', () => { it('user is presented with an error message', async () => { // arrange // act // assert }); }); });
ملاحظات:
- قبل إجراء جميع الاختبارات ، استخدم واجهة برمجة تطبيقات GitHub لعرض قائمة بالمستودعات عند استدعائها باسم مستخدم محدد.
- بعد كل اختبار ، نظف اختبار React DOM بحيث يبدأ كل اختبار من مكان نظيف.
-
describe
الكتل ، حدد حالة استخدام اختبار التكامل وتغيرات التدفق. - اختلافات التدفق التي نختبرها هي:
- يُدخل المستخدم اسم مستخدم صالحًا مرتبطًا بمستودعات GitHub العامة.
- يُدخل المستخدم اسم مستخدم صالحًا لا يحتوي على مستودعات GitHub العامة المرتبطة.
- يقوم المستخدم بإدخال اسم مستخدم غير موجود على GitHub.
-
it
بحظر استخدام رد الاتصال غير المتزامن لأن حالة الاستخدام التي يختبرونها بها خطوة غير متزامنة فيها.
كتابة اختبار التدفق الأول
أولاً ، يحتاج التطبيق إلى العرض.
const { getByText, getByPlaceholderText, queryByText } = render(<App />);
طريقة render
التي تم استيرادها من الوحدة النمطية @testing-library/react
تُستخدم هذه الاستعلامات لتحديد موقع عناصر DOM للتفاعل معها والتأكيد عليها.

الآن ، كخطوة أولى في التدفق قيد الاختبار ، يُقدم للمستخدم حقل اسم مستخدم ويكتب سلسلة اسم مستخدم فيه.
userEvent.type(getByPlaceholderText('userSelection.usernamePlaceholder'), FAKE_USERNAME_WITH_REPOS);
المساعد userEvent
من وحدة @testing-library/user-event
تم استيرادها له أسلوب type
يحاكي سلوك المستخدم عندما يكتب نصًا في حقل نصي. يقبل معلمتين: عنصر DOM الذي يقبل الإدخال والسلسلة التي يكتبها المستخدم.
عادة ما يجد المستخدمون عناصر DOM بالنص المرتبط بها. في حالة الإدخال ، يكون إما نص تسمية أو نص عنصر نائب. يتيح لنا أسلوب الاستعلام getByPlaceholderText
تم إرجاعه سابقًا من render
العثور على عنصر DOM من خلال نص العنصر النائب.
يرجى ملاحظة أنه نظرًا لأن النص نفسه غالبًا ما يتغير ، فمن الأفضل عدم الاعتماد على قيم الترجمة الفعلية وبدلاً من ذلك تكوين وحدة الترجمة لإرجاع مفتاح عنصر الترجمة كقيمة له.
على سبيل المثال ، عندما يُرجع التعريب "en-US" عادةً Enter GitHub username
كقيمة لمفتاح userSelection.usernamePlaceholder
، في الاختبارات ، نريده إرجاع userSelection.usernamePlaceholder
.
عندما يكتب المستخدم نصًا في حقل ، يجب أن يرى قيمة حقل النص محدثة.
expect(getByPlaceholderText('userSelection.usernamePlaceholder')).toHaveAttribute('value', FAKE_USERNAME_WITH_REPOS);
بعد ذلك في التدفق ، ينقر المستخدم على زر الإرسال ويتوقع رؤية قائمة المستودعات.
userEvent.click(getByText('userSelection.submitButtonText').closest('button')); getByText('repositories.header');
تحاكي طريقة userEvent.click
نقر المستخدم على عنصر DOM ، بينما يعثر استعلام getByText
على عنصر DOM بالنص الذي يحتوي عليه. يضمن closest
معدل أن نختار العنصر من النوع الصحيح.
ملاحظة: في اختبارات التكامل ، غالبًا ما تخدم الخطوات كلاً من أدوار act
assert
. على سبيل المثال ، نؤكد أنه يمكن للمستخدم النقر فوق الزر بالنقر فوقه.
في الخطوة السابقة ، أكدنا أن المستخدم يرى قسم قائمة المستودعات في التطبيق. الآن ، نحتاج إلى التأكيد على أنه نظرًا لأن جلب قائمة المستودعات من GitHub قد يستغرق بعض الوقت ، يرى المستخدم إشارة إلى أن الجلب قيد التقدم. نريد أيضًا التأكد من أن التطبيق لا يخبر المستخدم بأنه لا توجد مستودعات مرتبطة باسم المستخدم الذي تم إدخاله بينما لا يزال يتم جلب قائمة المستودعات.
getByText('repositories.loadingText'); expect(queryByText('repositories.empty')).toBeNull();
لاحظ أنه يتم استخدام بادئة الاستعلام getBy
للتأكيد على إمكانية العثور على عنصر DOM ، كما أن بادئة الاستعلام عن queryBy
الاستعلام مفيدة للتأكيد المعاكس. أيضًا ، لا queryBy
عن طريق الخطأ إذا لم يتم العثور على عنصر.
بعد ذلك ، نريد التأكد من انتهاء التطبيق في النهاية من جلب المستودعات وعرضها على المستخدم.
await waitForElement(() => REPOS_LIST.reduce((elementsToWaitFor, repository) => { elementsToWaitFor.push(getByText(repository.name)); elementsToWaitFor.push(getByText(repository.description)); return elementsToWaitFor; }, []));
يتم استخدام طريقة waitForElement
غير المتزامنة لانتظار تحديث DOM الذي سيعرض التأكيد المقدم كمعامل أسلوب صحيح. في هذه الحالة ، نؤكد أن التطبيق يعرض الاسم والوصف لكل مستودع يتم إرجاعه بواسطة واجهة برمجة تطبيقات GitHub المزعجة.
أخيرًا ، يجب ألا يعرض التطبيق مؤشرًا على أنه يتم جلب المستودعات ولا يجب أن يعرض رسالة خطأ.
expect(queryByText('repositories.loadingText')).toBeNull(); expect(queryByText('repositories.error')).toBeNull();
يبدو اختبار تكامل React الناتج كما يلي:
it('user can view the list of public repositories for entered GitHub username', async () => { const { getByText, getByPlaceholderText, queryByText } = render(<App />); userEvent.type(getByPlaceholderText('userSelection.usernamePlaceholder'), FAKE_USERNAME_WITH_REPOS); expect(getByPlaceholderText('userSelection.usernamePlaceholder')).toHaveAttribute('value', FAKE_USERNAME_WITH_REPOS); userEvent.click(getByText('userSelection.submitButtonText').closest('button')); getByText('repositories.header'); getByText('repositories.loadingText'); expect(queryByText('repositories.empty')).toBeNull(); await waitForElement(() => REPOS_LIST.reduce((elementsToWaitFor, repository) => { elementsToWaitFor.push(getByText(repository.name)); elementsToWaitFor.push(getByText(repository.description)); return elementsToWaitFor; }, [])); expect(queryByText('repositories.loadingText')).toBeNull(); expect(queryByText('repositories.error')).toBeNull(); });
اختبارات التدفق البديلة
عندما يقوم المستخدم بإدخال اسم مستخدم GitHub بدون مستودعات عامة مرتبطة ، يعرض التطبيق رسالة مناسبة.
describe('when GitHub user has no public repositories', () => { it('user is presented with a message that there are no public repositories for entered GitHub username', async () => { const { getByText, getByPlaceholderText, queryByText } = render(<App />); userEvent.type(getByPlaceholderText('userSelection.usernamePlaceholder'), FAKE_USERNAME_WITHOUT_REPOS); expect(getByPlaceholderText('userSelection.usernamePlaceholder')).toHaveAttribute('value', FAKE_USERNAME_WITHOUT_REPOS); userEvent.click(getByText('userSelection.submitButtonText').closest('button')); getByText('repositories.header'); getByText('repositories.loadingText'); expect(queryByText('repositories.empty')).toBeNull(); await waitForElement(() => getByText('repositories.empty')); expect(queryByText('repositories.error')).toBeNull(); }); });
عندما يقوم المستخدم بإدخال اسم مستخدم GitHub غير موجود ، يعرض التطبيق رسالة خطأ.
describe('when GitHub user does not exist', () => { it('user is presented with an error message', async () => { const { getByText, getByPlaceholderText, queryByText } = render(<App />); userEvent.type(getByPlaceholderText('userSelection.usernamePlaceholder'), FAKE_BAD_USERNAME); expect(getByPlaceholderText('userSelection.usernamePlaceholder')).toHaveAttribute('value', FAKE_BAD_USERNAME); userEvent.click(getByText('userSelection.submitButtonText').closest('button')); getByText('repositories.header'); getByText('repositories.loadingText'); expect(queryByText('repositories.empty')).toBeNull(); await waitForElement(() => getByText('repositories.error')); expect(queryByText('repositories.empty')).toBeNull(); }); });
لماذا اختبارات تكامل التفاعل روك
يوفر اختبار التكامل حقًا مكانًا رائعًا لتطبيقات React. تساعد هذه الاختبارات في اكتشاف الأخطاء واستخدام نهج TDD بينما ، في نفس الوقت ، لا تتطلب الصيانة عند تغيير التنفيذ.
تُعد مكتبة اختبار React ، التي تم عرضها في هذه المقالة ، أداة رائعة لكتابة اختبارات تكامل React ، حيث تتيح لك التفاعل مع التطبيق كما يفعل المستخدم والتحقق من حالة التطبيق وسلوكه من منظور المستخدم.
نأمل أن تساعدك الأمثلة المقدمة هنا على البدء في كتابة اختبارات التكامل على مشاريع React الجديدة والحالية. يمكن العثور على نموذج التعليمات البرمجية الكامل الذي يتضمن تنفيذ التطبيق على موقع GitHub الخاص بي.