تطوير React.js القائم على الاختبار: اختبار وحدة React.js باستخدام إنزيم و Jest
نشرت: 2022-03-11وفقًا لمايكل فيذرز ، يُقال إن أي جزء من التعليمات البرمجية لا يحتوي على اختبارات هو رمز قديم. لذلك ، فإن أحد أفضل الطرق لتجنب إنشاء رمز قديم هو استخدام التطوير القائم على الاختبار (TDD).
في حين أن هناك العديد من الأدوات المتاحة لاختبار وحدات JavaScript و React.js ، في هذا المنشور ، سنستخدم Jest و Enzyme لإنشاء مكون React.js بوظائف أساسية باستخدام TDD.
لماذا نستخدم TDD لإنشاء مكون React.js؟
تقدم TDD العديد من الفوائد إلى التعليمات البرمجية الخاصة بك - تتمثل إحدى مزايا تغطية الاختبار العالية في أنها تتيح إعادة بناء التعليمات البرمجية بسهولة مع الحفاظ على الكود الخاص بك نظيفًا وعمليًا.
إذا قمت بإنشاء مكون React.js من قبل ، فقد أدركت أن الشفرة يمكن أن تنمو بسرعة كبيرة. تمتلئ بالكثير من الشروط المعقدة التي تسببها البيانات المتعلقة بتغييرات الحالة واستدعاءات الخدمة.
كل مكون يفتقر إلى اختبارات الوحدة له كود قديم يصعب الحفاظ عليه. يمكننا إضافة اختبارات الوحدة بعد إنشاء كود الإنتاج. ومع ذلك ، قد نخاطر بتجاهل بعض السيناريوهات التي كان يجب اختبارها. من خلال إنشاء الاختبارات أولاً ، لدينا فرصة أكبر لتغطية كل سيناريو منطقي في مكوننا ، مما يجعل من السهل إعادة البناء والصيانة.
كيف نقوم باختبار الوحدة على مكون React.js؟
هناك العديد من الاستراتيجيات التي يمكننا استخدامها لاختبار مكون React.js:
- يمكننا التحقق من استدعاء وظيفة معينة في
props
عند إرسال حدث معين. - يمكننا أيضًا الحصول على نتيجة وظيفة
render
نظرًا لحالة المكون الحالي ومطابقتها مع تخطيط محدد مسبقًا. - يمكننا حتى التحقق مما إذا كان عدد العناصر الفرعية للمكون يتطابق مع الكمية المتوقعة.
من أجل استخدام هذه الاستراتيجيات ، سنستخدم أداتين مفيدتين للعمل مع الاختبارات في React.js: Jest و Enzyme.
استخدام النكتة لإنشاء اختبارات الوحدة
Jest هو إطار اختبار مفتوح المصدر تم إنشاؤه بواسطة Facebook ولديه تكامل رائع مع React.js. يتضمن أداة سطر أوامر لتنفيذ الاختبار على غرار ما تقدمه Jasmine و Mocha. كما يسمح لنا أيضًا بإنشاء وظائف وهمية بتكوين يكاد يكون صفريًا ويوفر مجموعة رائعة حقًا من المطابقات التي تسهل قراءة التأكيدات.
علاوة على ذلك ، فإنه يوفر ميزة رائعة حقًا تسمى "اختبار اللقطة" ، والتي تساعدنا في التحقق من نتيجة عرض المكون والتحقق منها. سنستخدم اختبار اللقطة لالتقاط شجرة أحد المكونات وحفظها في ملف يمكننا استخدامه لمقارنتها مع شجرة العرض (أو أي شيء نمرره إلى الوظيفة expect
كحجة أولى.)
استخدام الإنزيم لتحميل مكونات React.js
يوفر الإنزيم آلية لتركيب أشجار مكونات React.js واجتيازها. سيساعدنا هذا في الوصول إلى ممتلكاتها الخاصة وحالتها وكذلك الدعائم التابعة لها من أجل تشغيل تأكيداتنا.
يوفر الإنزيم وظيفتين أساسيتين mount
المكونات: shallow
والتركيب. يتم تحميل الوظيفة shallow
في الذاكرة فقط المكون الجذر بينما يقوم mount
بتحميل شجرة DOM الكاملة.
سنقوم بدمج Enzyme و Jest لتركيب مكون React.js وتشغيل التأكيدات عليه.
تهيئة بيئتنا
يمكنك إلقاء نظرة على هذا الريبو ، الذي يحتوي على التكوين الأساسي لتشغيل هذا المثال.
نحن نستخدم الإصدارات التالية:
{ "react": "16.0.0", "enzyme": "^2.9.1", "jest": "^21.2.1", "jest-cli": "^21.2.1", "babel-jest": "^21.2.0" }
إنشاء مكون React.js باستخدام TDD
تتمثل الخطوة الأولى في إنشاء اختبار فاشل سيحاول تقديم مكون React.js باستخدام وظيفة الإنزيم الضحلة.
// MyComponent.test.js import React from 'react'; import { shallow } from 'enzyme'; import MyComponent from './MyComponent'; describe("MyComponent", () => { it("should render my component", () => { const wrapper = shallow(<MyComponent />); }); });
بعد إجراء الاختبار ، حصلنا على الخطأ التالي:
ReferenceError: MyComponent is not defined.
ثم نقوم بإنشاء المكون الذي يوفر البنية الأساسية لاجتياز الاختبار.
// MyComponent.js import React from 'react'; export default class MyComponent extends React.Component { render() { return <div />; } }
في الخطوة التالية ، سنتأكد من أن المكون الخاص بنا يعرض تخطيط واجهة مستخدم محددًا مسبقًا باستخدام وظيفة toMatchSnapshot
من Jest.
بعد استدعاء هذه الطريقة ، يقوم Jest تلقائيًا بإنشاء ملف لقطة يسمى [testFileName].snap
، والذي يضاف إلى المجلد __snapshots__
.
يمثل هذا الملف تخطيط واجهة المستخدم الذي نتوقعه من عرض المكون الخاص بنا.
ومع ذلك ، نظرًا لأننا نحاول إجراء TDD خالص ، يجب علينا إنشاء هذا الملف أولاً ثم استدعاء وظيفة toMatchSnapshot
لجعل الاختبار يفشل.
قد يبدو هذا محيرًا بعض الشيء ، نظرًا لأننا لا نعرف التنسيق الذي تستخدمه Jest لتمثيل هذا التنسيق.
قد تميل إلى تنفيذ وظيفة toMatchSnapshot
أولاً ورؤية النتيجة في ملف اللقطة ، وهذا خيار صالح. ومع ذلك ، إذا كنا نريد حقًا استخدام TDD الخالص ، فنحن بحاجة إلى معرفة كيفية تنظيم ملفات اللقطة.
يحتوي ملف اللقطة على تخطيط يطابق اسم الاختبار. هذا يعني أنه إذا كان اختبارنا يحتوي على هذا النموذج:
desc("ComponentA" () => { it("should do something", () => { … } });
يجب أن نحدد ذلك في قسم الصادرات: Component A should do something 1
.
يمكنك قراءة المزيد عن اختبار اللقطات هنا.
لذلك ، نقوم أولاً بإنشاء ملف MyComponent.test.js.snap
.
//__snapshots__/MyComponent.test.js.snap exports[`MyComponent should render initial layout 1`] = ` Array [ <div> <input type="text" /> </div>, ] `;
بعد ذلك ، نقوم بإنشاء اختبار الوحدة الذي سيتحقق من تطابق اللقطة مع العناصر المكونة الفرعية.

// MyComponent.test.js ... it("should render initial layout", () => { // when const component = shallow(<MyComponent />); // then expect(component.getElements()).toMatchSnapshot(); }); ...
يمكننا اعتبار components.getElements
نتيجة لطريقة العرض.
نقوم بتمرير هذه العناصر إلى طريقة expect
من أجل تشغيل التحقق مقابل ملف اللقطة.
بعد إجراء الاختبار حصلنا على الخطأ التالي:
Received value does not match stored snapshot 1. Expected: - Array [ <div> <input type="text” /> </div>, ] Actual: + Array []
يخبرنا Jest أن النتيجة من component.getElements
لا تتطابق مع اللقطة. لذلك ، نجعل هذا الاختبار يمر عن طريق إضافة عنصر الإدخال في MyComponent
.
// MyComponent.js import React from 'react'; export default class MyComponent extends React.Component { render() { return <div><input type="text" /></div>; } }
الخطوة التالية هي إضافة وظيفة إلى input
عن طريق تنفيذ وظيفة عندما تتغير قيمتها. نقوم بذلك بتحديد دالة في خاصية onChange
.
نحتاج أولاً إلى تغيير اللقطة لفشل الاختبار.
//__snapshots__/MyComponent.test.js.snap exports[`MyComponent should render initial layout 1`] = ` Array [ <div> <input onChange={[Function]} type="text" /> </div>, ] `;
عيب تعديل اللقطة أولاً هو أن ترتيب الخاصيات (أو السمات) مهم.
ستقوم Jest بفرز الدعائم المستلمة في دالة expect
أبجديًا قبل التحقق منها مقابل اللقطة. لذا ، يجب أن نحددها بهذا الترتيب.
بعد إجراء الاختبار حصلنا على الخطأ التالي:
Received value does not match stored snapshot 1. Expected: - Array [ <div> onChange={[Function]} <input type="text”/> </div>, ] Actual: + Array [ <div> <input type=”text” /> </div>, ]
لجعل هذا الاختبار ناجحًا ، يمكننا ببساطة توفير وظيفة فارغة لـ onChange
.
// MyComponent.js import React from 'react'; export default class MyComponent extends React.Component { render() { return <div><input onChange={() => {}} type="text" /></div>; } }
بعد ذلك ، نتأكد من أن حالة المكون تتغير بعد إرسال حدث onChange
.
للقيام بذلك ، نقوم بإنشاء اختبار وحدة جديد والذي سيستدعي وظيفة onChange
في الإدخال عن طريق تمرير حدث لتقليد حدث حقيقي في واجهة المستخدم.
بعد ذلك ، نتحقق من أن حالة المكون تحتوي على مفتاح يسمى input
.
// MyComponent.test.js ... it("should create an entry in component state", () => { // given const component = shallow(<MyComponent />); const form = component.find('input'); // when form.props().onChange({target: { name: 'myName', value: 'myValue' }}); // then expect(component.state('input')).toBeDefined(); });
نحصل الآن على الخطأ التالي.
Expected value to be defined, instead received undefined
يشير هذا إلى أن المكون ليس له خاصية في الحالة تسمى input
.
نجعل الاختبار يمر عن طريق تعيين هذا الإدخال في حالة المكون.
// MyComponent.js import React from 'react'; export default class MyComponent extends React.Component { render() { return <div><input onChange={(event) => {this.setState({input: ''})}} type="text" /></div>; } }
بعد ذلك ، نحتاج إلى التأكد من تعيين القيمة في إدخال الحالة الجديد. سوف نحصل على هذه القيمة من الحدث.
لذلك ، دعنا ننشئ اختبارًا يتأكد من احتواء الحالة على هذه القيمة.
// MyComponent.test.js ... it("should create an entry in component state with the event value", () => { // given const component = shallow(<MyComponent />); const form = component.find('input'); // when form.props().onChange({target: { name: 'myName', value: 'myValue' }}); // then expect(component.state('input')).toEqual('myValue'); }); ~~~ Not surprisingly, we get the following error. ~~ Expected value to equal: "myValue" Received: ""
نجحنا أخيرًا في اجتياز هذا الاختبار عن طريق الحصول على القيمة من الحدث وتعيينه كقيمة إدخال.
// MyComponent.js import React from 'react'; export default class MyComponent extends React.Component { render() { return <div><input onChange={(event) => { this.setState({input: event.target.value})}} type="text" /></div>; } }
بعد التأكد من اجتياز جميع الاختبارات ، يمكننا إعادة تشكيل الكود الخاص بنا.
يمكننا استخراج الوظيفة التي تم تمريرها في خاصية onChange
إلى وظيفة جديدة تسمى updateState
.
// MyComponent.js import React from 'react'; export default class MyComponent extends React.Component { updateState(event) { this.setState({ input: event.target.value }); } render() { return <div><input onChange={this.updateState.bind(this)} type="text" /></div>; } }
لدينا الآن مكون React.js بسيط تم إنشاؤه باستخدام TDD.
ملخص
في هذا المثال ، حاولنا استخدام TDD الخالص باتباع كل خطوة في كتابة أقل رمز ممكن للفشل واجتياز الاختبارات.
قد تبدو بعض الخطوات غير ضرورية وقد نميل إلى تخطيها. ومع ذلك ، كلما تخطينا أي خطوة ، سننتهي باستخدام نسخة أقل نقاءً من TDD.
يعد استخدام عملية TDD أقل صرامة أمرًا صالحًا أيضًا وقد يعمل بشكل جيد.
نصيحتي لك هي تجنب تخطي أي خطوات وعدم الشعور بالضيق إذا وجدت ذلك صعبًا. TDD هي تقنية ليس من السهل إتقانها ، لكنها بالتأكيد تستحق القيام بها.
إذا كنت مهتمًا بمعرفة المزيد عن TDD والتطور المدفوع بالسلوك المرتبط به (BDD) ، فاقرأ رئيسك لن يقدر TDD بواسطة زميلك Toptaler Ryan Wilcox.