اختبارات الانحدار البصري. تمهيد

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

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

تحسين الأداء


بدأنا ، بشكل غريب بما فيه الكفاية ، مع الأداء الأمثل. ساوضح لماذا. تعتمد اختباراتنا المرئية على Storybook . ليست كل قصة في كتاب القصة عبارة عن مكون واحد ، بل "كتلة" كاملة (على سبيل المثال ، شبكة بها قائمة من الكيانات أو بطاقة كيان أو حوار أو حتى التطبيق ككل). لعرض هذه الكتلة ، تحتاج إلى "ضخ" القصة بالبيانات ، ليس فقط البيانات المعروضة للمستخدم ، ولكن أيضًا حالة المكونات المستخدمة داخل الكتلة. يتم تخزين هذه المعلومات جنبا إلى جنب مع شفرة المصدر في شكل ملفات json التي تحتوي على تمثيل متسلسل لحالة التطبيق (مخزن الإرجاع). نعم ، هذه البيانات ، بعبارة ملطفة ، لا لزوم لها ، لكنها تبسط بشكل كبير إنشاء الاختبارات. لإنشاء اختبار جديد ، نفتح ببساطة البطاقة أو القائمة أو مربع الحوار المطلوبين في التطبيق ، ونلقي نظرة سريعة على الحالة الحالية للتطبيق ونسلسلها إلى ملف. ثم نضيف قصة جديدة واختبارات تأخذ لقطات شاشة لهذه القصة (كل ذلك في بضعة أسطر من الكود).

هذا النهج يزيد حتما من حجم الحزمة. درجة ازدواجية البيانات الموجودة فيه "تتحول". عند إجراء الاختبارات ، ينفّذ محرك الجوزاء كل مجموعة اختبار في جلسة متصفح منفصلة. تقوم كل جلسة بتحميل الحزمة من جديد وحجم الحزمة في مثل هذا المخطط بعيد عن القيمة الأخيرة.

لتقليل وقت تشغيل الاختبار ، قمنا بتقليل عدد مجموعات الاختبار عن طريق زيادة عدد الاختبارات فيها. وبالتالي ، يمكن أن تؤثر مجموعة الاختبار على قصة متعددة في وقت واحد. في هذا المخطط ، فقدنا عملياً القدرة على "فرز" مساحة معينة فقط من الشاشة نظرًا لحقيقة أن Gemini يسمح لك بتعيين منطقة لقطة الشاشة فقط لمجموعة الاختبارات ككل (على الرغم من أن واجهة برمجة التطبيقات تتيح لك القيام بذلك قبل كل لقطة شاشة ، ولكنها في الواقع لا تعمل).

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

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

لذلك ، من أجل تسريع عملية تحميل مجموعة الاختبار ، قمنا بتقسيم الحزمة المتجانسة إلى أجزاء ، مع تخصيص كل لقطة لحالة التطبيق إلى "قطعة" منفصلة ، يتم تحميلها "عند الطلب" لكل قصة على حدة. يبدو رمز إنشاء القصة الآن كما يلي:

// visual-regression.stories.js import React from 'react'; import StoryProvider from './story-provider'; const stories = storiesOf('visual-regression', module); [ { name: 'Contract', loadData: import('./snapshots/contract.testdata') }, { name: 'ExecutionTask', loadData: import('./snapshots/execution-task.testdata') }, { name: 'ExecutionAssignment', loadData: import('./snapshots/execution-assignment.testdata') }, { name: 'DocumentTemplate', loadData: import('./snapshots/document-template.testdata') }, { name: 'Explorer', loadData: import('./snapshots/explorer.testdata') }, { name: 'Inbox', loadData: import('./snapshots/inbox.testdata') }, ] .map(story => { stories .add(story.name, () => <StoryProvider loadSnapshot={story.loadData} />) .add(`${story.name}Dark`, () => <StoryProvider loadSnapshot={story.loadData} theme='night' />); }); 

لإنشاء قصة ، يتم استخدام مكون StoryProvider (سيتم إعطاء الرمز الخاص به أدناه). يتم تحميل اللقطات باستخدام وظيفة الاستيراد الديناميكية . تختلف القصص المختلفة عن بعضها البعض فقط في صور الدول. بالنسبة للسمة المظلمة ، يتم إنشاء قصتها الخاصة ، باستخدام نفس اللقطة التي تميز قصة السمة الخفيفة. في سياق القصص القصيرة ، يبدو كما يلي:

قصة الموضوع الافتراضي


قصة الظلام الموضوع


يقبل مكون StoryProvider رد اتصال لتحميل لقطة تسمى وظيفة import (). تعمل وظيفة import () بشكل غير متزامن ، لذلك لا يمكنك التقاط لقطة شاشة مباشرة بعد تحميل المجموعة النصية - فنحن نخاطر بإزالة الفراغ. للوقوف على لحظة نهاية التنزيل ، يعرض المزود عنصر DOM الذي يشير إلى محرك الاختبار طوال فترة التنزيل ، والتي يجب تأجيلها مع لقطة الشاشة:

قصة provider.js
 // story-provider.js const propsStub = { // -,      . . . }; type Props = { loadSnapshot: () => Object, theme: ?string }; const StoryProvider = (props: Props) => { const [ snapshotState, setsnapshotState ] = React.useState(null); React.useEffect(() => { //    (async() => setsnapshotState((await props.loadSnapshot).default))(); }); if (!snapshotState) //     ,     return <div className={'loading-stub'}>Loading...</div>; //    snapshotState.metadata = require('./snapshots/metadata'); //  redux-   const store = createMockStore(snapshotState); //   applyTheme(props.theme); return ( <Provider store={store}> <MemoryRouter> <App {...propsStub} /> </MemoryRouter> </Provider> ); }; export default StoryProvider; 


بالإضافة إلى ذلك ، لتقليل حجم الحزمة ، قم بتعطيل إضافة خرائط المصدر إلى الحزمة. ولكن حتى لا تفقد القدرة على تصحيح القصة (أنت لا تعرف أبدًا ما الذي يحدث) ، فنحن نفعل ذلك وفقًا للشروط التالية:

.storybook / webpack.config.js
 // .storybook/webpack.config.js ... module.exports = { ... devtool: process.env.NODE_ENV === 'vr-test' ? '(none)' : 'eval-source-map' }; 


package.json
 // package.json { ... "scripts": { ... "storybook": "start-storybook", "build-storybook": "cross-env NODE_ENV=vr-test build-storybook -o ./storybook-static", ... }, 


يقوم البرنامج النصي بناء مجموعة نصية تشغيل npm بترجمة مجموعة قصص ثابتة دون sourcemap إلى مجلد statbook. يتم استخدامه عند إجراء الاختبارات. ويتم استخدام البرنامج النصي لتشغيل القصص القصيرة npm لتطوير وتصحيح قصص الاختبار.

القضاء على الازدواجية في المعلومات البصرية


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

في الواقع ، لماذا هيرميون؟ حاليًا ، يتم وضع علامة على مستودع الجوزاء على أنه مهمل ، وكان علينا "التحرك" في مكان ما عاجلاً أم آجلاً. هيكل ملف التكوين Hermione مطابق لهيكل ملف التكوين Gemini وتمكنا من إعادة استخدام هذا التكوين. الجوزاء وهيرميون الإضافات شائعة أيضا. بالإضافة إلى ذلك ، تمكنا من إعادة استخدام البنية التحتية للاختبار - الأجهزة الافتراضية وشبكة السيلينيوم المنشورة.

على عكس Gemini ، لا يتم وضع Hermione كأداة فقط لاختبار الانحدار للتخطيط. قدرات معالجة المستعرض الخاص به أوسع بكثير ومحدودة فقط من خلال إمكانيات Webdriver IO . في تركيبة مع المخاوي ، يعتبر هذا المحرك مناسبًا للاستخدام أكثر للاختبارات الوظيفية (محاكاة إجراءات المستخدم) أكثر من اختبار التخطيط. بالنسبة لاختبار الانحدار للتخطيط ، يوفر Hermione طريقة assertView () فقط ، والتي تقارن لقطة شاشة لصفحة متصفح مع مرجع. يمكن أن تقتصر لقطة الشاشة على المساحة المحددة باستخدام محددات css.

بالنسبة لحالتنا ، فإن اختبار كل قصة فردية سيبدو كما يلي:

 //    describe('Visual regression', function() { it('Contract card should equal to etalon', function() { return this.browser //  story   .url('http://localhost:8080/iframe.html?selectedKind=visual-regression&selectedStory=ContractDark') // ,      story .waitForVisible('.loading-stub', true) //          .assertView('layout', '.form'); }) }); 

أسلوب waitForVisible () ، على الرغم من اسمه ، يسمح لك أن تتوقع ليس فقط المظهر ، ولكن أيضًا إخفاء العنصر ، إذا قمت بتعيين المعلمة الثانية على true. نستخدمها هنا لانتظار إخفاء عنصر علامة ، مما يشير إلى أن لقطة البيانات لم يتم تحميلها بعد وأن القصة ليست جاهزة بعد لقطة شاشة.

إذا حاولت العثور على طريقة waitForVisible () في وثائق Hermione ، فلن تجد أي شيء. الحقيقة هي أن أسلوب waitForVisible () هو أسلوب Webdriver IO API . طريقة url () ، على التوالي ، أيضًا. في طريقة url () ، نعبر عنوان الإطار لقصة معينة ، وليس كتاب القصة بالكامل. أولاً ، يعد هذا ضروريًا حتى لا يتم عرض قائمة الأخبار في نافذة المتصفح - لسنا بحاجة إلى اختبارها. ثانياً ، إذا لزم الأمر ، يمكننا الوصول إلى عناصر DOM داخل الإطار (تسمح لك أساليب webdriverIO بتنفيذ تعليمات JavaScript البرمجية في سياق المستعرض).

لتبسيط كتابة الاختبارات ، قمنا بتجميع غلافنا على اختبارات mocha. الحقيقة هي أنه لا يوجد معنى خاص في التفصيل المفصل لحالات الاختبار لاختبار الانحدار. جميع حالات الاختبار هي نفسها - "يجب أن تساوي etalon". حسنًا ، لا أريد تكرار رمز انتظار تحميل البيانات في كل اختبار. لذلك ، يتم تفويض نفس العمل لجميع اختبارات "القرود" إلى وظيفة المجمع ، ويتم كتابة الاختبارات نفسها بشكل تعريفي (جيد ، تقريبًا). هنا نص هذه الوظيفة:

إنشاء اختبار suite.js
 const themes = [ 'default', 'dark' ]; const rootClassName = '.explorer'; const loadingStubClassName = '.loading-stub'; const timeout = 2000; function createTestSuite(testSuite) { const { name, storyName, browsers, testCases, selector } = testSuite; //  ,       browsers && hermione.only.in(browsers); //      themes.forEach(theme => { describe(`${name}_${theme}`, () => it('should equal to etalon', function() { let browser = this.browser //   story .url(`${storybookUrl}/iframe.html?selectedKind=visual-regression&selectedStory=${storyName}-${theme}`) //     .waitForVisible(loadingStubClassName, timeout, true) .waitForVisible(rootClassName); //    (  ) if (testCases && testCases.length > 0) { testCases.forEach(testCase => { if (testCase.before) browser = testCase.before(browser); browser = browser.assertView(`${name}__${testCase.name}_${theme}`, testCase.selector || selector || rootClassName, testCase.options); }); return browser; } //    ,    return browser.assertView(`${name}_${theme}`, selector || rootClassName); })); }); } 


يتم تمرير كائن يصف مجموعة الاختبار إلى إدخال الوظيفة. تم تصميم كل مجموعة اختبار وفقًا للسيناريو التالي: التقط لقطة شاشة للتخطيط الرئيسي (على سبيل المثال ، منطقة بطاقة كيان أو منطقة قائمة كيان) ، ثم اضغط برمجيًا على أزرار يمكن أن تؤدي إلى ظهور عناصر أخرى (على سبيل المثال ، اللوحات المنبثقة أو قوائم السياق) و "التقاط لقطة شاشة" »كل عنصر من هذا القبيل على حدة. وبالتالي ، نحاكي إجراءات المستخدم في المتصفح ، ولكن ليس بهدف اختبار نوع من سيناريو العمل ، ولكن ببساطة "التقاط" أقصى عدد ممكن من المكونات المرئية. علاوة على ذلك ، فإن ازدواجية المعلومات المرئية في لقطات هو الحد الأدنى ، لأنه تؤخذ لقطات الشاشة "pointwise" باستخدام محددات. مثال جناح الاختبار:

المستكشف-suite.js
 // explorer-suite.js //         module.exports = { //  story,    storyName: 'explorer', //    name: 'explorer', //  ,       browsers: [ 'chrome-1920x1080', 'ie-1920x1080' ], //   testCases: [ { //    name: 'layout' }, { //    name: 'notification-area', selector: '.notification-area__popup', before: b => b .click('.notification-area__popup-button') .waitForVisible('.notification-area__popup') .execute(function() { //       document.querySelectorAll('.expandable-item__content')[2].click(); }) }, //... ] }; 


tests.js
 // tests.js [ require('./suites/explorer-suite'), //... ] .forEach(suite => createTestSuite(suite)); 


تحديد التغطية


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

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

لتقييم التغطية في Chrome DevTools ، هناك أداة تحمل اسم التغطية مناسبة جدًا لهذه الحالة:



تتيح لك التغطية تحديد الأنماط أو كود js الذي تم استخدامه عند التعامل مع صفحة المتصفح. يشير تقرير استخدام الخطوط الخضراء إلى الكود المستخدم ، الأحمر - غير المستخدم. وكل شيء سيكون على ما يرام إذا كان لدينا تطبيق على مستوى "hello، world" ، ولكن ماذا نفعل عندما يكون لدينا الآلاف من أسطر الكود؟ لقد فهم مطورو التغطية هذا جيدًا وقدموا القدرة على تصدير التقرير إلى ملف يمكن بالفعل إعداده برمجيًا.

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

بعد إجراء الاختبارات في الوضع اليدوي ، نحصل على تقرير تغطية ، وهو ملف json. في التقرير لكل css ، js ، ts ، إلخ. يشير الملف إلى نصه (في سطر واحد) وفواصل الكود المستخدمة في هذا النص (في شكل مؤشرات أحرف هذا السطر). أدناه جزء من التقرير:

coverage.json
 [ { "url": "http://localhost:6006/theme-default.css", "ranges": [ { "start": 0, "end": 8127 } ], "text": "... --theme_primary-accent: #5b9bd5;\r\n --theme_primary-light: #ffffff;\r\n --theme_primary: #f4f4f4;\r\n ..." }, { "url": "http://localhost:6006/main.css", "ranges": [ { "start": 0, "end": 610 }, { "start": 728, "end": 754 } ] "text": "... \r\n line-height:1;\r\n}\r\n\r\nol, ul{\r\n list-style:none;\r\n}\r\n\r\nblockquote, q..." ] 


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

هنا ، التعبيرات العادية تساعدنا. بالطبع ، لن يعملوا إلا إذا تم استيفاء اصطلاحات التسمية لفئات css (في الشفرة الخاصة بنا ، تتم تسمية فئات css وفقًا لمنهجية BEM - block_name_name_name_modifier). باستخدام التعبيرات المعتادة ، نحسب القيم الفريدة لأسماء المجموعات ، والتي لم تعد من الصعب ربطها بالمكونات. بالطبع ، نحن مهتمون أيضًا بالعناصر والمعدلات ، ولكن ليس في المقام الأول ، نحتاج أولاً إلى التعامل مع "سمكة" أكبر. في ما يلي نص برمجي لمعالجة تقرير التغطية

coverage.js
 const modules = require('./coverage.json').filter(e => e.url.endsWith('.css')); function processRange(module, rangeStart, rangeEnd, isUsed) { const rules = module.text.slice(rangeStart, rangeEnd); if (rules) { const regex = /^\.([^\d{:,)_ ]+-?)+/gm; const classNames = rules.match(regex); classNames && classNames.forEach(name => selectors[name] = selectors[name] || isUsed); } } let previousEnd, selectors = {}; modules.forEach(module => { previousEnd = 0; for (const range of module.ranges) { processRange(module, previousEnd, range.start, false); processRange(module, range.start, range.end, true); previousEnd = range.end; } processRange(module, previousEnd, module.length, false); }); console.log('className;isUsed'); Object.keys(selectors).sort().forEach(s => { console.log(`${s};${selectors[s]}`); }); 


نقوم بتنفيذ البرنامج النصي عن طريق وضع ملف cover.json الذي تم تصديره أولاً من Chrome DevTools وكتابة العادم إلى ملف .csv:

عقدة التغطية. js> cover.csv

يمكنك فتح هذا الملف باستخدام excel وتحليل البيانات ، بما في ذلك تحديد النسبة المئوية لتغطية الرمز عن طريق الاختبارات.



بدلا من السيرة الذاتية


إن استخدام مجموعة القصص القصيرة كأساس للاختبارات المرئية له ما يبرره تمامًا - لدينا درجة كافية من تغطية كود CSS من خلال الاختبارات التي تحتوي على عدد صغير نسبيًا من القصص وتكاليف الحد الأدنى لإنشاء قصص جديدة.

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

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

Source: https://habr.com/ru/post/ar479040/


All Articles