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

أرنب تجريبي
أولاً ، وصف موجز للأدوات المستخدمة. على سبيل المثال للتنفيذ ، سأأخذ SerenityJS من التقرير الأصلي ، مع TypeScript كلغة نصوص اختبار.
سنضع تجارب على تطبيق TODO - تطبيق لإنشاء قائمة بسيطة من المهام. على سبيل المثال ، سوف نستخدم الكود من جان مولاك ، منشئ المكتبة (كود TypeScript ، وبالتالي فإن التمييز قد ذهب قليلاً).
لنبدأ في عام 2009
في هذا الوقت ، ظهر Selenium WebDriver وبدأ الناس في استخدامه. لسوء الحظ ، في عدد كبير من الحالات ، بسبب حداثة التكنولوجيا والافتقار إلى خبرة البرمجة ، فمن الخطأ. لقد تبين أن الاختبارات عبارة عن لصق نسخ كامل وغير مدعوم و "هش".
والنتيجة هي ردود فعل سلبية وسمعة سيئة مع autotests. في النهاية ، استجاب سيمون ستيوارت ، مؤلف كتاب سيلينيوم ويب دريفر ، غيابياً في مقالته "اختبارات السلينيوم ليست مستقرة!" . الرسالة العامة: "إذا كانت اختباراتك هشة ولا تعمل بشكل جيد - فهذا ليس بسبب السيلينيوم ، ولكن لأن الاختبارات نفسها ليست مكتوبة للغاية."
لفهم ما هو على المحك ، دعونا نلقي نظرة على سيناريو المستخدم التالي:
Feature: Add new items to the todo list In order to avoid having to remember things that need doing As a forgetful person I want to be able to record what I need to do in a place where I won't forget about them Scenario: Adding an item to a list with other items Given that James has a todo list containing Buy some cookies, Walk the dog When he adds Buy some cereal to his list Then his todo list should contain Buy some cookies, Walk the dog, Buy some cereal
التنفيذ الساذج "على الجبين" سيبدو كما يلي:
import { browser, by, element, protractor } from 'protractor'; export = function todoUserSteps() { this.Given(/^.*that (.*) has a todo list containing (.*)$/, (name: string, items: string, callback: Function) => { browser.get('http://todomvc.com/examples/angularjs/'); browser.driver.manage().window().maximize(); listOf(items).forEach(item => { element(by.id('new-todo')).sendKeys(item, protractor.Key.ENTER); }); browser.driver.controlFlow().execute(callback); }); this.When(/^s?he adds (.*?) to (?:his|her) list$/, (itemName: string, callback: Function) => { element(by.id('new-todo')) .sendKeys(itemName, protractor.Key.ENTER) .then(callback); }); this.Then(/^.* todo list should contain (.*?)$/, (items: string, callback: Function) => { expect(element.all(by.repeater('todo in todos')).getText()) .to.eventually.eql(listOf(items)) .and.notify(callback); }); };
كما ترون ، يتم استخدام واجهة برمجة التطبيقات ذات المستوى المنخفض هنا ، والتعامل مع DOM ، ولصق نسخ محددات css. من الواضح أنه في المستقبل ، حتى مع حدوث تغيير بسيط في واجهة المستخدم ، سيتعين عليك تغيير الكود في العديد من الأماكن.
كحل ، كان على السيلينيوم تقديم شيء جيد بما فيه الكفاية للتخلص من هذه المشاكل ، وفي الوقت نفسه يمكن الوصول إليه للأشخاص الذين لديهم خبرة قليلة أو معدومة في البرمجة الموجهة للكائنات. كان هذا الحل هو "كائن الصفحة" ، وهو نمط لتنظيم التعليمات البرمجية.
يصفه Martin Fowler بأنه كائن تجريدي حول صفحة HTML أو جزءها ، مما يتيح لك التفاعل مع عناصر الصفحة دون لمس HTML نفسه. من الناحية المثالية ، يجب أن يسمح مثل هذا الكائن لرمز العميل بالقيام برؤية كل ما يمكن للشخص القيام به ورؤيته.

بعد إعادة كتابة مثالنا وفقًا لهذا النمط ، لدينا ما يلي:
import { browser, by, element, protractor } from 'protractor'; class TodoList { What_Needs_To_Be_Done = element(by.id('new-todo')); Items = element.all(by.repeater('todo in todos')); addATodoItemCalled(itemName: string): PromiseLike<void> { return this.What_Needs_To_Be_Done.sendKeys(itemName, protractor.Key.ENTER); } displayedItems(): PromiseLike<string[]> { return this.Items.getText(); } } export = function todoUserSteps() { let todoList = new TodoList(); this.Given(/^.*that (.*) has a todo list containing (.*)$/, (name: string, items: string, callback: Function) => { browser.get('http://todomvc.com/examples/angularjs/'); browser.driver.manage().window().maximize(); listOf(items).forEach(item => { todoList.addATodoItemCalled(item); }); browser.driver.controlFlow().execute(callback); }); this.When(/^s?he adds (.*?) to (?:his|her) list$/, (itemName: string, callback: Function) => { todoList.addATodoItemCalled(itemName).then(() => callback()); }); this.Then(/^.* todo list should contain (.*?)$/, (items: string, callback: Function) => { expect(todoList.displayedItems()) .to.eventually.eql(listOf(items)) .and.notify(callback); }); };
للوهلة الأولى ، يبدو جيدًا - لقد تخلصنا من الازدواجية والتعبئة الإضافية. ولكن هناك مشكلة واحدة: غالبًا ما تنمو هذه الفئات في عملية التطوير إلى أحجام هائلة .

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

إذا حاولت فصل هذه الفئة بناءً على مبادئ SOLID ، فستحصل على شيء مثل التالي:

نموذج المجال
المشكلة التالية التي لدينا مع كائن الصفحة هي أنه يعمل على الصفحات. في أي اختبار قبول ، من الشائع استخدام قصص المستخدمين. تتناسب التنمية المدفوعة بالسلوك (BDD) ولغة Gherkin بشكل مثالي مع هذا النموذج. من المفهوم أن مسار المستخدم إلى الهدف (السيناريو) أكثر أهمية من التنفيذ المحدد. على سبيل المثال ، إذا استخدمت أداة تسجيل دخول في جميع الاختبارات ، فعند تغيير طريقة تسجيل الدخول (تم نقل النموذج ، وتحويله إلى الدخول الموحد) ، فسيتعين عليك تغيير جميع الاختبارات. في المشروعات الديناميكية بشكل خاص ، يمكن أن يكون هذا مضيعة للوقت ويستغرق الكثير من الوقت ويشجع الفريق على تأجيل اختبارات الكتابة حتى تستقر الصفحات (حتى لو كان كل شيء واضحًا مع حالات الاستخدام).
لحل هذه المشكلة ، يجدر النظر إليها من الجانب الآخر. نقدم المفاهيم التالية:
- الأدوار - لمن كل هذا؟
- الأهداف - لماذا هم (المستخدمون) هنا وما الذي يريدون تحقيقه؟
- المهام - ما الذي يتعين عليهم القيام به لتحقيق هذه الأهداف؟
- الإجراءات - كيف يجب أن يتفاعل المستخدم مع الصفحة بالضبط لإكمال المهمة؟
وبالتالي ، يصبح كل برنامج نصي للاختبار ، في الواقع ، سيناريو للمستخدم ، يهدف إلى تنفيذ قصة مستخدم محددة. إذا قمت بدمج هذا النهج مع مبادئ البرمجة الموجهة للكائنات الموضحة أعلاه ، فستكون النتيجة عبارة عن نقش سيناريو (أو رحلة مستخدم ). تم التعبير عن أفكاره ومبادئه لأول مرة في عام 2007 ، حتى قبل PageObject.
سيناريو
نعيد كتابة الكود الخاص بنا باستخدام المبادئ الناتجة.
جيمس هو الممثل الذي سيلعب مستخدمنا في هذا السيناريو. إنه يعرف كيفية استخدام المتصفح.
let james = Actor.named('James').whoCan(BrowseTheWeb.using(protractor.browser));
هدف جيمس هو إضافة العنصر الأول إلى قائمته:
Scenario: Adding an item to a list with other items
لتحقيق هذا الهدف ، يحتاج جيمس إلى ما يلي:
- ابدأ بقائمة تحتوي على عدة عناصر ،
- إضافة عنصر جديد.
يمكننا تقسيم هذا إلى فصلين:
import { PerformsTasks, Task } from 'serenity-js/lib/screenplay'; import { Open } from 'serenity-js/lib/screenplay-protractor'; import { AddATodoItem } from './add_a_todo_item'; export class Start implements Task { static withATodoListContaining(items: string[]) { return new Start(items); } performAs(actor: PerformsTasks) { return actor.attemptsTo( Open.browserOn('/examples/angularjs/'), ...this.addAll(this.items) ); } constructor(private items: string[]) { } private addAll(items: string[]): Task[] { return items.map(item => AddATodoItem.called(item)); } }
تتطلب واجهة Task
تحديد طريقة performAs
، حيث سيتم نقل الممثل إلى أثناء التنفيذ. attemptsTo
- وظيفة Combinator ، يقبل أي عدد من المراوغات. وبالتالي ، يمكنك بناء مجموعة متنوعة من تسلسل. في الواقع ، كل ما تفعله هذه المهمة هو فتح متصفح على الصفحة المطلوبة وإضافة عناصر هناك. الآن دعونا نلقي نظرة على مهمة إضافة عنصر:
import { PerformsTasks, Task } from 'serenity-js/lib/screenplay'; import { Enter } from 'serenity-js/lib/screenplay-protractor'; import { protractor } from 'protractor'; import { TodoList } from '../components/todo_list'; export class AddATodoItem implements Task { static called(itemName: string) { return new AddATodoItem(itemName); }
إنه أكثر إثارة للاهتمام هنا ، تظهر الإجراءات ذات المستوى المنخفض - أدخل النص في عنصر الصفحة ثم اضغط على Enter. TodoList
هو كل ما تبقى من الجزء "الوصفي" من كائن الصفحة ، وهنا لدينا محددات css:
import { Target, Question, Text } from 'serenity-js/lib/screenplay-protractor'; import { by } from 'protractor'; export class TodoList { static What_Needs_To_Be_Done = Target .the('"What needs to be done?" input box') .located(by.id('new-todo')); static Items = Target .the('List of Items') .located(by.repeater('todo in todos')); static Items_Displayed = Text.ofAll(TodoList.Items); }
حسنًا ، يبقى التحقق من أنه بعد كل التلاعب يتم عرض المعلومات الصحيحة. تقدم SerenityJS واجهة Question<T>
- كيان يقوم بإرجاع قيمة معروضة أو قائمة قيم ( Text.ofAll
في المثال أعلاه). كيف يمكن للمرء تطبيق سؤال بإرجاع نص عنصر HTML:
export class Text implements Question<string> { public static of(target: Target): Text { return new Text(target); } answeredBy(actor: UsesAbilities): PromiseLike<string[]> { return BrowseTheWeb.as(actor).locate(this.target).getText(); } constructor(private target: Target) { } }
ما هو مهم ، ربط المتصفح اختياري. BrowseTheWeb
هو مجرد القدرة ، والتي تتيح لك التفاعل مع المتصفح. يمكنك تنفيذ ، على سبيل المثال ، قدرة RecieveEmails
، والتي تسمح للممثل بقراءة الرسائل (للتسجيل على الموقع).
بتجميعها معًا ، نحصل على هذا المخطط (من Jan Molak ):

والسيناريو التالي:
let actor: Actor; this.Given(/^.*that (.*) has a todo list containing (.*)$/, function (name: string, items: string) { actor = Actor.named(name).whoCan(BrowseTheWeb.using(protractor.browser)); return actor.attemptsTo( Start.withATodoListContaining(listOf(items)) ); }); this.When(/^s?he adds (.*?) to (?:his|her) list$/, function (itemName: string) { return actor.attemptsTo( AddATodoItem.called(itemName) ) }); this.Then(/^.* todo list should contain (.*?)$/, function (items: string) { return expect(actor.toSee(TodoList.Items_Displayed)).eventually.deep.equal(listOf(items)) });
في البداية ، تبدو النتيجة ضخمة بعض الشيء ، لكن مع نمو النظام ، سيزداد مقدار التعليمات البرمجية المعاد استخدامها ، وستؤثر تغييرات التخطيط والصفحة فقط على محددات css والمهام / الأسئلة منخفضة المستوى ، مما يترك البرامج النصية والمهام عالية المستوى بدون تغيير عمليًا.
التطبيقات
إذا تحدثنا عن المكتبات ، وليس عن محاولات المدن الصغيرة لتطبيق هذا النمط ، فإن الأكثر شيوعًا هو Serenity BDD for Java. يستخدم Javascript / TypeScript منفذ SerenityJS الخاص به. خارج الصندوق ، تعرف كيف تعمل مع Cucumber و Mocha.
بحث سريع أنتجت أيضا مكتبة ل. NET - الهدوء . لا أستطيع أن أقول أي شيء عنها ، لأنني لم أقابلها من قبل.
عند استخدام Serenity و SerenityJS ، يمكنك استخدام الأداة المساعدة لإنشاء التقارير .
تقرير مع الصورخذ الكود باستخدام Mocha:
describe('Finding things to do', () => { describe('James can', () => { describe('remove filters so that the list', () => { it('shows all the items', () => Actor.named('James').attemptsTo( Start.withATodoListContaining([ 'Write some code', 'Walk the dog' ]), CompleteATodoItem.called('Write some code'), FilterItems.toShowOnly('Active'), FilterItems.toShowOnly('All'), Ensure.theListOnlyContains('Write some code', 'Walk the dog'), )); }); }); });
سيتضمن التقرير إحصائيات عامة:

وتفاصيل الخطوات مع لقطات الشاشة:

المراجع
بعض المقالات ذات الصلة:
- كائنات الصفحة المعاد تشكيلها: الخطوات الصلبة لنمط السيناريو / رحلة
- ما وراء كائنات الصفحة: أتمتة اختبار الجيل التالي مع الصفاء ونمط السيناريو
- الصفاء BDD ونمط سيناريو
الأوراق:
المكتبات:
- الصفاء BDD - جافا
- الصفاء JS - JavaScript / TypeScript
- الهدوء -. NET
يرجى كتابة التعليقات إذا كنت تستخدم أنماطًا / طرقًا أخرى لتنظيم مدونة الاختبارات الذاتية.