كيف تجعل مكونات React اختبار واجهة المستخدم أمرًا سهلاً
نشرت: 2022-03-11اختبار النهايات الخلفية سهل. يمكنك اختيار لغتك المفضلة ، وإقرانها بإطار العمل المفضل لديك ، وكتابة بعض الاختبارات ، والضغط على "تشغيل". وحدة التحكم الخاصة بك تقول "رائع! إنها تعمل!" تجري خدمة التكامل المستمر الخاصة بك اختباراتك في كل مرة ، فالحياة رائعة.
من المؤكد أن التطوير المستند إلى الاختبار (TDD) غريب في البداية ، ولكن البيئة التي يمكن التنبؤ بها ، ومتسابقي الاختبار المتعددين ، وأدوات الاختبار المخبوزة في أطر العمل ، ودعم التكامل المستمر ، تجعل الحياة سهلة. قبل خمس سنوات اعتقدت أن الاختبارات هي الحل لكل مشكلة واجهتها على الإطلاق.
ثم أصبح العمود الفقري كبيرًا.
لقد تحولنا جميعًا إلى واجهة MVC الأمامية. أصبحت الخلفيات القابلة للاختبار لدينا خوادم قواعد بيانات مجيدة. انتقلت التعليمات البرمجية الأكثر تعقيدًا إلى المتصفح. ولم تعد تطبيقاتنا قابلة للاختبار في الممارسة العملية.
ذلك لأن اختبار كود الواجهة الأمامية ومكونات واجهة المستخدم صعب نوعًا ما.
ليس الأمر سيئًا للغاية إذا كان كل ما نريده هو التحقق من أن عارضاتنا يتصرفن بشكل جيد. أو أن استدعاء دالة سيغير القيمة الصحيحة. كل ما نحتاج إلى القيام به لاختبار وحدة React هو:
- اكتب وحدات نمطية جيدة التكوين ومعزولة.
- استخدم اختبارات الياسمين أو الموكا (أو أيًا كان) لتشغيل الوظائف.
- استخدم عداء اختبار ، مثل Karma أو Chutzpah.
هذا هو. كودنا هو اختبار الوحدة.
كان من المعتاد أن يكون إجراء اختبارات الواجهة الأمامية هو الجزء الصعب. كان لكل إطار أفكاره الخاصة وفي معظم الحالات انتهى بك الأمر بنافذة متصفح يمكنك تحديثها يدويًا في كل مرة تريد فيها إجراء الاختبارات. بالطبع ، ستنسى دائمًا. على الأقل ، أعلم أنني فعلت.
في عام 2012 ، أصدرت Vojta Jina عداء Karma (كان يسمى Testacular في ذلك الوقت). مع Karma ، يصبح اختبار الواجهة الأمامية مواطنًا كاملاً في سلسلة الأدوات. تعمل اختبارات React الخاصة بنا في محطة طرفية أو على خادم تكامل مستمر ، وتعيد تشغيل نفسها عندما نغير ملفًا ، ويمكننا أيضًا اختبار الكود الخاص بنا في متصفحات متعددة في نفس الوقت.
ماذا نتمنى أكثر من ذلك؟ حسنًا ، لاختبار كود الواجهة الأمامية بالفعل .
يتطلب اختبار الواجهة الأمامية أكثر من مجرد اختبارات وحدة
يعد اختبار الوحدة أمرًا رائعًا: إنه أفضل طريقة لمعرفة ما إذا كانت الخوارزمية تفعل الشيء الصحيح في كل مرة ، أو للتحقق من منطق التحقق من صحة الإدخال ، أو تحويلات البيانات ، أو أي عملية أخرى معزولة. اختبار الوحدة مثالي للأساسيات.
لكن التعليمات البرمجية للواجهة الأمامية لا تتعلق بمعالجة البيانات. يتعلق الأمر بأحداث المستخدم وتقديم العروض الصحيحة في الوقت المناسب. الواجهات الأمامية تدور حول المستخدمين.
إليك ما نريد أن نكون قادرين على القيام به:
- اختبار أحداث المستخدم في React
- اختبر الاستجابة لتلك الأحداث
- تأكد من عرض الأشياء الصحيحة في الوقت المناسب
- قم بإجراء الاختبارات في العديد من المتصفحات
- أعد تشغيل الاختبارات على تغييرات الملف
- العمل مع أنظمة التكامل المستمر مثل ترافيس
خلال السنوات العشر التي كنت أفعل فيها هذا ، لم أجد طريقة مناسبة لاختبار تفاعل المستخدم وعرض العرض حتى بدأت في الضغط على React.
اختبار وحدة التفاعل: مكونات واجهة المستخدم
React هو أسهل طريقة لتحقيق هذه الأهداف. يعود ذلك جزئيًا إلى الطريقة التي تجبرنا بها على تصميم التطبيقات باستخدام أنماط قابلة للاختبار ، ويرجع ذلك جزئيًا إلى وجود أدوات اختبار React رائعة.
إذا لم تكن قد استخدمت React من قبل ، فعليك مراجعة كتابي React + d3.js. إنها موجهة نحو التصورات ، لكن قيل لي إنها "مقدمة رائعة وخفيفة الوزن" لـ React.
يجبرنا رد الفعل على بناء كل شيء على أنه "مكونات". يمكنك التفكير في مكونات React على أنها أدوات ، أو أجزاء من HTML مع بعض المنطق. إنهم يتبعون العديد من أفضل مبادئ البرمجة الوظيفية ، باستثناء أنهم كائنات.
على سبيل المثال ، بالنظر إلى نفس مجموعة المعلمات ، سيعرض مكون React نفس المخرجات دائمًا. بغض النظر عن عدد المرات التي يتم تقديمها ، بغض النظر عمن يقوم بتقديمها ، بغض النظر عن مكان وضع الإخراج. نفس الشيء دائما. نتيجة لذلك ، لا يتعين علينا إجراء سقالات معقدة لاختبار مكونات React. إنهم يهتمون فقط بخصائصهم ، ولا يحتاجون إلى تتبع المتغيرات العالمية وكائنات التكوين.
نحقق ذلك في جزء كبير منه بتجنب الدولة. يمكنك استدعاء هذه الشفافية المرجعية في البرمجة الوظيفية. لا أعتقد أن هناك اسمًا خاصًا لهذا في React ، لكن المستندات الرسمية توصي بتجنب استخدام الحالة قدر الإمكان.
عندما يتعلق الأمر باختبار تفاعلات المستخدم ، فقد غطتنا React بأحداث مرتبطة بوظائف الاسترجاعات. من السهل إعداد جواسيس اختبار والتأكد من أن حدث النقر يستدعي الوظيفة الصحيحة. ونظرًا لأن مكونات React تعرض نفسها ، يمكننا فقط تشغيل حدث نقرة والتحقق من التغييرات في HTML. ينجح هذا لأن مكون React لا يهتم إلا بنفسه. النقر هنا لا يغير الأمور هناك . لن نضطر أبدًا إلى التعامل مع مجموعة من معالجات الأحداث ، فقط استدعاءات دالة محددة جيدًا.
أوه ، ولأن React سحر ، فلا داعي للقلق بشأن DOM. يستخدم React ما يسمى بـ DOM الظاهري لتحويل المكونات إلى متغير JavaScript. والإشارة إلى DOM الظاهري هي كل ما نحتاجه لاختبار مكونات React ، حقًا.
انها جميلة جدا.
اختبار React's TestUtils
تأتي React مع مجموعة من TestUtils
. حتى أن هناك عداء اختبار موصى به يسمى Jest ، لكنني لا أحب ذلك. سأشرح لماذا بعد قليل. أولا ، TestUtils
.
نحصل عليها عن طريق القيام بشيء مثل require('react/addons').addons.TestUtils
. هذه هي نقطة دخولنا لاختبار تفاعلات المستخدم والتحقق من المخرجات.
يتيح لنا TestUtils
عرض مكون React عن طريق وضع DOM الخاص به في متغير ، بدلاً من إدراجه في صفحة. على سبيل المثال ، لتصيير مكون React ، سنفعل شيئًا كالتالي:
var component = TestUtils.renderIntoDocument( <MyComponent /> );
ثم يمكننا استخدام TestUtils
للتحقق مما إذا كان قد تم تقديم جميع العناصر الفرعية. شيء من هذا القبيل:
var h1 = TestUtils.findRenderedDOMComponentWithTag( component, 'h1' );
سوف يقوم findRenderedDOMComponentWithTag
بما يبدو مثل: تصفح الأطفال والعثور على المكون الذي نبحث عنه وإعادته. ستتصرف القيمة المُعادة كمكوِّن React.
يمكننا بعد ذلك استخدام getDOMNode()
للوصول إلى عنصر DOM الخام واختبار قيمه. للتحقق من أن علامة h1
في المكون تقول "A title" ، نكتب هذا:
expect(h1.getDOMNode().textContent) .toEqual("A title");
معًا ، سيبدو الاختبار الكامل كما يلي:
it("renders an h1", function () { var component = TestUtils.renderIntoDocument( <MyComponent /> ); var h1 = TestUtils.findRenderedDOMComponentWithTag( component, 'h1' ); expect(h1.getDOMNode().textContent) .toEqual("A title"); });
الجزء الرائع هو أن TestUtils يتيح لنا تشغيل أحداث المستخدم أيضًا. بالنسبة إلى حدث النقر ، نكتب شيئًا كالتالي:
var node = component .findRenderedDOMComponentWithTag('button') .getDOMNode(); TestUtils.Simulate.click(node);
هذا يحاكي النقرة ويطلق أي مستمعين محتملين ، والتي يجب أن تكون طرق مكونة تغير إما المخرجات ، أو الحالة ، أو كليهما. يمكن لهؤلاء المستمعين استدعاء دالة على المكون الرئيسي إذا لزم الأمر.
جميع الحالات سهلة الاختبار: الحالة المتغيرة هي في component.state
، يمكننا الوصول إلى المخرجات بوظائف DOM العادية ، واستدعاءات الوظائف مع الجواسيس.
لماذا ليس الدعابة؟
توصي وثائق React الرسمية باستخدام https://facebook.github.io/jest/ كإجراء اختبار وإطار عمل اختبار React. Jest مبني على الياسمين ويستخدم نفس التركيب. علاوة على كل ما تحصل عليه من Jasmine ، يسخر Jest أيضًا من كل شيء باستثناء المكون الذي نختبره. هذا رائع من الناحية النظرية ، لكنني أجده مزعجًا. أي شيء لم ننفذه بعد ، أو يأتي من جزء مختلف من قاعدة الكود ، هو فقط undefined
. في حين أن هذا أمر جيد في كثير من الحالات ، إلا أنه يمكن أن يؤدي إلى أخطاء فاشلة بهدوء.
لقد واجهت مشكلة في اختبار حدث النقر ، على سبيل المثال. بغض النظر عما جربته ، لن ينادي مستمعها. ثم أدركت أن Jest قد سخرت من الوظيفة ولم تخبرني بذلك أبدًا.
لكن أسوأ جريمة ارتكبها Jest ، إلى حد بعيد ، كانت تتمثل في عدم وجود وضع مشاهدة لاختبار التغييرات الجديدة تلقائيًا. يمكننا تشغيله مرة واحدة ، والحصول على نتائج الاختبار ، وهذا كل شيء. (أحب إجراء اختباراتي في الخلفية أثناء العمل. وإلا نسيت تشغيلها). في الوقت الحاضر لم تعد هذه مشكلة.
أوه ، و Jest لا يدعم تشغيل اختبارات React في متصفحات متعددة. هذه مشكلة أقل مما كانت عليه من قبل ، لكنني أشعر أنها ميزة مهمة لتلك المناسبة النادرة ، لا يحدث خطأ heisenbug إلا في إصدار محدد من Chrome ...
ملاحظة المحرر: منذ كتابة هذه المقالة في الأصل ، تحسنت Jest بشكل كبير. يمكنك قراءة برنامجنا التعليمي الأحدث ، اختبار وحدة التفاعل باستخدام الإنزيم والدعابة ، وتقرر بنفسك ما إذا كان اختبار Jest على مستوى المهمة في الوقت الحاضر.
اختبار التفاعل: مثال متكامل
على أي حال ، لقد رأينا كيف يجب أن يعمل اختبار React الجيد للواجهة الأمامية من الناحية النظرية. دعنا نضعها موضع التنفيذ مع مثال قصير.
سنقوم بتصور طرق مختلفة لتوليد أرقام عشوائية باستخدام مكون مخطط التشتت المصنوع باستخدام React و d3.js. الكود والعرض التوضيحي موجودان أيضًا على Github.
سنستخدم Karma كجهاز اختبار ، و Mocha كإطار اختبار ، و Webpack كمحمل للوحدات النمطية.
وانشاء
ستدخل ملفات المصدر الخاصة بنا في دليل <root>/src
، وسنضع اختبارات في دليل <root>/src/__tests__
. الفكرة هي أنه يمكننا وضع عدة أدلة داخل src
، واحدة لكل مكون رئيسي ، ولكل منها ملفات الاختبار الخاصة به. يؤدي تجميع كود المصدر واختبار الملفات مثل هذا إلى تسهيل إعادة استخدام مكونات React في مشاريع مختلفة.
مع وجود بنية الدليل في مكانها الصحيح ، يمكننا تثبيت التبعيات مثل هذا:
$ npm install --save-dev react d3 webpack babel-loader karma karma-cli karma-mocha karma-webpack expect
إذا فشل تثبيت أي شيء ، فحاول إعادة تشغيل هذا الجزء من التثبيت. يفشل NPM أحيانًا بطرق تختفي عند إعادة التشغيل.

يجب أن يبدو ملف package.json
الخاص بنا بهذا الشكل عندما ننتهي:
// package.json { "name": "react-testing-example", "description": "A sample project to investigate testing options with ReactJS", "scripts": { "test": "karma start" }, // ... "homepage": "https://github.com/Swizec/react-testing-example", "devDependencies": { "babel-core": "^5.2.17", "babel-loader": "^5.0.0", "d3": "^3.5.5", "expect": "^1.6.0", "jsx-loader": "^0.13.2", "karma": "^0.12.31", "karma-chrome-launcher": "^0.1.10", "karma-cli": "0.0.4", "karma-mocha": "^0.1.10", "karma-sourcemap-loader": "^0.3.4", "karma-webpack": "^1.5.1", "mocha": "^2.2.4", "react": "^0.13.3", "react-hot-loader": "^1.2.7", "react-tools": "^0.13.3", "webpack": "^1.9.4", "webpack-dev-server": "^1.8.2" } }
بعد بعض التهيئة ، سنتمكن من إجراء الاختبارات إما npm test
أو karma start
.
التكوين
ليس هناك الكثير للتكوين. علينا التأكد من أن Webpack يعرف كيفية العثور على الكود الخاص بنا ، وأن Karma يعرف كيفية إجراء الاختبارات.
وضعنا سطرين من JavaScript في ملف ./tests.webpack.js
لمساعدة Karma و Webpack على اللعب معًا:
// tests.webpack.js var context = require.context('./src', true, /-test\.jsx?$/); context.keys().forEach(context);
هذا يخبر -test
بأن يعتبر أي شيء له لاحقة اختبار جزءًا من مجموعة الاختبار.
يستغرق تكوين الكارما مزيدًا من العمل:
// karma.conf.js var webpack = require('webpack'); module.exports = function (config) { config.set({ browsers: ['Chrome'], singleRun: true, frameworks: ['mocha'], files: [ 'tests.webpack.js' ], preprocessors: { 'tests.webpack.js': ['webpack'] }, reporters: ['dots'], webpack: { module: { loaders: [ {test: /\.jsx?$/, exclude: /node_modules/, loader: 'babel-loader'} ] }, watch: true }, webpackServer: { noInfo: true } }); };
معظم هذه الخطوط من تهيئة Karma الافتراضية. استخدمنا browsers
لنقول إن الاختبارات يجب أن تعمل في Chrome ، frameworks
لتحديد إطار عمل الاختبار الذي نستخدمه ، و singleRun
لإجراء الاختبارات مرة واحدة بشكل افتراضي. يمكنك الاستمرار في تشغيل الكارما في الخلفية مع karma start --no-single-run
.
هؤلاء الثلاثة واضحون. تعد عناصر Webpack أكثر إثارة للاهتمام.
نظرًا لأن Webpack يتعامل مع شجرة تبعية الكود الخاص بنا ، فلا يتعين علينا تحديد جميع ملفاتنا في مصفوفة files
. نحتاج فقط إلى tests.webpack.js
، والذي يتطلب بعد ذلك جميع الملفات الضرورية.
نستخدم إعداد حزمة الويب لإخبار webpack
بما يجب القيام به. في البيئة العادية ، ينتقل هذا الجزء إلى ملف webpack.config.js
.
كما نطلب من Webpack استخدام أداة تحميل babel-loader
لجافا سكريبت. يمنحنا هذا جميع الميزات الجديدة الرائعة من ECMAScript2015 و React's JSX.
باستخدام تكوين webpackServer
، نطلب من Webpack عدم طباعة أي معلومات تصحيح. سوف يفسد فقط مخرجات الاختبار لدينا.
مكون التفاعل والاختبار
مع مجموعة اختبار التشغيل ، فإن الباقي بسيط. يتعين علينا إنشاء مكون يقبل مصفوفة من الإحداثيات العشوائية وينشئ عنصر <svg>
بمجموعة من النقاط.
بعد أفضل ممارسات اختبار React - أي ممارسة TDD القياسية - سنكتب الاختبار أولاً ، ثم مكون React الفعلي. لنبدأ بملف اختبارات الفانيليا في src/__tests__/
:
// ScatterPlot-test.jsx var React = require('react/addons'), TestUtils = React.addons.TestUtils, expect = require('expect'), ScatterPlot = require('../ScatterPlot.jsx'); var d3 = require('d3'); describe('ScatterPlot', function () { var normal = d3.random.normal(1, 1), mockData = d3.range(5).map(function () { return {x: normal(), y: normal()}; }); });
نحتاج أولاً إلى React و TestUtils و expect
والمكتبة المتوقعة والشفرة التي نختبرها. ثم نصنع مجموعة اختبار جديدة مع describe
، وننشئ بعض البيانات العشوائية.
في اختبارنا الأول ، دعنا نتأكد من أن ScatterPlot
يعرض عنوانًا. يذهب اختبارنا داخل كتلة describe
:
// ScatterPlot-test.jsx it("renders an h1", function () { var scatterplot = TestUtils.renderIntoDocument( <ScatterPlot /> ); var h1 = TestUtils.findRenderedDOMComponentWithTag( scatterplot, 'h1' ); expect(h1.getDOMNode().textContent).toEqual("This is a random scatterplot"); });
ستتبع معظم الاختبارات نفس النمط:
- يقدم - يجعل.
- ابحث عن عقدة محددة.
- تحقق من المحتويات.
كما رأينا سابقًا ، يعرض renderIntoDocument
المكون الخاص بنا ، ويجد findRenderedDOMComponentWithTag
الجزء المحدد الذي نختبره ، ويعطينا getDOMNode
وصولاً خامًا إلى DOM.
في البداية سيفشل اختبارنا. لجعله يمر ، علينا كتابة المكون الذي يعرض علامة العنوان:
var React = require('react/addons'); var d3 = require('d3'); var ScatterPlot = React.createClass({ render: function () { return ( <div> <h1>This is a random scatterplot</h1> </div> ); } }); module.exports = ScatterPlot;
هذا هو. يعرض مكون ScatterPlot
<div>
بعلامة <h1>
تحتوي على النص المتوقع ، وسوف ينجح اختبارنا. نعم ، إنها أطول من مجرد HTML ، لكن تحمل معي.
ارسم بقية البومة
يمكنك مشاهدة بقية مثالنا على GitHub ، كما هو مذكور أعلاه. سنتخطى وصفها خطوة بخطوة في هذه المقالة ، لكن العملية العامة هي نفسها المذكورة أعلاه. مع ذلك ، أريد أن أريكم اختبارًا أكثر تشويقًا. اختبار يضمن ظهور جميع نقاط البيانات على الرسم البياني:
// ScatterPlot-test.jsx it("renders a circle for each datapoint", function () { var scatterplot = TestUtils.renderIntoDocument( <ScatterPlot data={mockData} /> ); var circles = TestUtils.scryRenderedDOMComponentsWithTag( scatterplot, 'circle' ); expect(circles.length).toEqual(5); });
نفسه كما كان من قبل. تقديم ، والعثور على العقد ، والتحقق من النتيجة. الجزء المثير هنا هو رسم عُقد DOM تلك. نضيف بعض سحر d3.js إلى مكون ScatterPlot
، مثل هذا:
// ScatterPlot.jsx componentWillMount: function () { this.yScale = d3.scale.linear(); this.xScale = d3.scale.linear(); this.update_d3(this.props); }, componentWillReceiveProps: function (newProps) { this.update_d3(newProps); }, update_d3: function (props) { this.yScale .domain([d3.min(props.data, function (d) { return dy; }), d3.max(props.data, function (d) { return dy; })]) .range([props.point_r, Number(props.height-props.point_r)]); this.xScale .domain([d3.min(props.data, function (d) { return dx; }), d3.max(props.data, function (d) { return dx; })]) .range([props.point_r, Number(props.width-props.point_r)]); }, ...
نحن نستخدم componentWillMount
لإعداد مقاييس d3 فارغة لمجالي X و Y ، و componentWillReceiveProps
للتأكد من تحديثهما عندما يتغير شيء ما. ثم update_d3
من تعيين domain
range
لكلا المقياسين.
سنستخدم المقياسين للترجمة بين القيم العشوائية في مجموعة البيانات والمواضع على الصورة. تُرجع معظم المولدات العشوائية أرقامًا في النطاق [0،1] ، وهو صغير جدًا بحيث لا يمكن رؤيته كوحدات بكسل.
ثم نضيف النقاط إلى طريقة العرض الخاصة بالمكون:
// ScatterPlot.jsx render: function () { return ( <div> <h1>This is a random scatterplot</h1> <svg width={this.props.width} height={this.props.height}> {this.props.data.map(function (pos, i) { var key = "circle-"+i; return ( <circle key={key} cx={this.xScale(pos.x)} cy={this.yScale(pos.y)} r={this.props.point_r} /> ); }.bind(this))}; </svg> </div> ); }
يمر هذا الرمز عبر صفيف this.props.data
ويضيف عنصر <circle>
لكل نقطة بيانات. بسيط.
إذا كنت تريد معرفة المزيد حول الجمع بين React و d3.js لإنشاء مكونات تصور البيانات ، فهذا سبب رائع آخر للتحقق من كتابي ، React + d3.js.
اختبار مكونات التفاعل الآلي: أسهل مما يبدو
هذا كل ما علينا معرفته حول كتابة مكونات أمامية قابلة للاختبار باستخدام React. لرؤية المزيد من مكونات React التي تختبر الكود ، تحقق من نموذج كود اختبار React على Github ، كما هو مذكور أعلاه.
لقد تعلمنا أن:
- يجبرنا رد الفعل على تشكيل نمط معياري وتغليفه.
- هذا يجعل اختبار React UI سهل التشغيل الآلي.
- اختبارات الوحدة ليست كافية للواجهات الأمامية.
- كارما هي عداء اختبار عظيم.
- تتمتع Jest بإمكانيات ، لكنها لم تتحقق بعد. (أو ربما هو كذلك الآن).
إذا أعجبك هذا المقال ، فاتبعني على Twitter واترك تعليقًا أدناه. شكرًا على القراءة ، واختبار رد فعل سعيد!