Mirage JS Deep Dive: استخدام Mirage JS و Cypress لاختبار واجهة المستخدم (الجزء 4)
نشرت: 2022-03-10أحد اقتباساتي المفضلة حول اختبار البرامج مأخوذ من وثائق Flutter. انها تقول:
"كيف يمكنك التأكد من أن تطبيقك يستمر في العمل مع إضافة المزيد من الميزات أو تغيير الوظائف الحالية؟ عن طريق كتابة الاختبارات ".
في هذه الملاحظة ، سيركز هذا الجزء الأخير من سلسلة Mirage JS Deep Dive على استخدام Mirage لاختبار تطبيق JavaScript الأمامي الخاص بك.
ملاحظة : تفترض هذه المقالة بيئة السرو. Cypress هو إطار اختبار لاختبار واجهة المستخدم. ومع ذلك ، يمكنك نقل المعرفة هنا إلى أي بيئة أو إطار عمل لاختبار واجهة المستخدم تستخدمه.
قراءة الأجزاء السابقة من السلسلة:
- الجزء 1: فهم نماذج وجمعيات Mirage JS
- الجزء الثاني: فهم المصانع والتركيبات والمسلسلات
- الجزء 3: فهم التوقيت والاستجابة والعبور
UI Tests Primer
يعد اختبار واجهة المستخدم أو واجهة المستخدم أحد أشكال اختبار القبول الذي يتم إجراؤه للتحقق من تدفقات المستخدم لتطبيق الواجهة الأمامية الخاص بك. ينصب تركيز هذه الأنواع من اختبارات البرامج على المستخدم النهائي وهو الشخص الفعلي الذي سيتفاعل مع تطبيق الويب الخاص بك على مجموعة متنوعة من الأجهزة التي تتراوح من أجهزة الكمبيوتر المكتبية والمحمولة إلى الأجهزة المحمولة. سيتفاعل هؤلاء المستخدمون مع تطبيقك أو يتفاعلون معه باستخدام أجهزة الإدخال مثل لوحة المفاتيح أو الماوس أو شاشات اللمس. لذلك ، تتم كتابة اختبارات واجهة المستخدم لتقليد تفاعل المستخدم مع التطبيق الخاص بك في أقرب وقت ممكن.
لنأخذ موقع التجارة الإلكترونية على سبيل المثال. سيكون سيناريو اختبار واجهة المستخدم النموذجي هو:
- يمكن للمستخدم عرض قائمة المنتجات عند زيارة الصفحة الرئيسية.
قد تكون سيناريوهات اختبار واجهة المستخدم الأخرى:
- يمكن للمستخدم رؤية اسم المنتج في صفحة تفاصيل المنتج.
- يمكن للمستخدم النقر فوق الزر "إضافة إلى عربة التسوق".
- يمكن للمستخدم الخروج.
تحصل على هذه الفكرة، أليس كذلك؟
عند إجراء اختبارات واجهة المستخدم ، ستعتمد في الغالب على حالاتك الخلفية ، أي هل أعادت المنتجات أو حدث خطأ؟ يتمثل الدور الذي تلعبه Mirage في جعل حالات الخادم هذه متاحة لك لتعديلها حسب حاجتك. لذا بدلاً من تقديم طلب فعلي إلى خادم الإنتاج الخاص بك في اختبارات واجهة المستخدم الخاصة بك ، فإنك تقدم الطلب إلى خادم Mirage mock.
بالنسبة للجزء المتبقي من هذه المقالة ، سنقوم بإجراء اختبارات واجهة المستخدم على واجهة مستخدم وهمية لتطبيق التجارة الإلكترونية على الويب. لذلك دعونا نبدأ.
أول اختبار لدينا لواجهة المستخدم
كما ذكرنا سابقًا ، تفترض هذه المقالة بيئة السرو. يجعل Cypress اختبار واجهة المستخدم على الويب سريعًا وسهلاً. يمكنك محاكاة النقرات والتنقل ويمكنك زيارة المسارات برمجيًا في تطبيقك. راجع المستندات لمعرفة المزيد عن Cypress.
لذلك ، بافتراض أن Cypress و Mirage متاحان لنا ، فلنبدأ بتحديد وظيفة وكيل لطلب واجهة برمجة التطبيقات الخاصة بك. يمكننا القيام بذلك في ملف support/index.js
الخاص بإعداد Cypress الخاص بنا. فقط الصق الكود التالي في:
// cypress/support/index.js Cypress.on("window:before:load", (win) => { win.handleFromCypress = function (request) { return fetch(request.url, { method: request.method, headers: request.requestHeaders, body: request.requestBody, }).then((res) => { let content = res.headers.map["content-type"] === "application/json" ? res.json() : res.text() return new Promise((resolve) => { content.then((body) => resolve([res.status, res.headers, body])) }) }) } })
بعد ذلك ، في ملف bootstrapping للتطبيق الخاص بك ( main.js
لـ Vue ، index.js
لـ React) ، سنستخدم Mirage لتوكيل طلبات واجهة برمجة التطبيقات لتطبيقك إلى وظيفة handleFromCypress
فقط عند تشغيل Cypress. هذا هو الكود الخاص بذلك:
import { Server, Response } from "miragejs" if (window.Cypress) { new Server({ environment: "test", routes() { let methods = ["get", "put", "patch", "post", "delete"] methods.forEach((method) => { this[method]("/*", async (schema, request) => { let [status, headers, body] = await window.handleFromCypress(request) return new Response(status, headers, body) }) }) }, }) }
مع هذا الإعداد ، في أي وقت يتم تشغيل Cypress ، يعرف تطبيقك أنه يستخدم Mirage كخادم وهمي لجميع طلبات واجهة برمجة التطبيقات.
دعنا نواصل كتابة بعض اختبارات واجهة المستخدم. سنبدأ باختبار صفحتنا الرئيسية لمعرفة ما إذا كانت تحتوي على 5 منتجات معروضة. للقيام بذلك في Cypress ، نحتاج إلى إنشاء ملف homepage.test.js
في مجلد tests
في جذر دليل مشروعك. بعد ذلك ، سنطلب من Cypress القيام بما يلي:
- قم بزيارة الصفحة الرئيسية أي
/
الطريق - ثم قم بتأكيد ما إذا كانت تحتوي على عناصر li مع فئة
product
وتحقق أيضًا مما إذا كانت 5 في الأرقام.
ها هو الكود:
// homepage.test.js it('shows the products', () => { cy.visit('/'); cy.get('li.product').should('have.length', 5); });
ربما تكون قد خمنت أن هذا الاختبار سيفشل لأنه ليس لدينا خادم إنتاج يعيد 5 منتجات إلى تطبيق الواجهة الأمامية الخاص بنا. إذن ماذا نفعل؟ نحن نسخر من الخادم في ميراج! إذا أحضرنا Mirage ، فيمكنه اعتراض جميع مكالمات الشبكة في اختباراتنا. لنفعل هذا أدناه ونبدأ تشغيل خادم Mirage قبل كل اختبار في وظيفة beforeEach
وأيضًا إيقاف تشغيله في وظيفة afterEach
. يتم توفير كل من الدالتين beforeEach
و afterEach
بواسطة Cypress وتم إتاحتهما حتى تتمكن من تشغيل الكود قبل وبعد كل تشغيل اختباري في مجموعة الاختبار الخاصة بك - ومن هنا جاء الاسم. لذلك دعونا نرى رمز هذا:
// homepage.test.js import { Server } from "miragejs" let server beforeEach(() => { server = new Server() }) afterEach(() => { server.shutdown() }) it("shows the products", function () { cy.visit("/") cy.get("li.product").should("have.length", 5) })
حسنًا ، لقد وصلنا إلى مكان ما ؛ لقد قمنا باستيراد الخادم من Mirage وبدأنا تشغيله afterEach
beforeEach
التوالي. دعنا نبدأ بالسخرية من مورد منتجاتنا.
// homepage.test.js import { Server, Model } from 'miragejs'; let server; beforeEach(() => { server = new Server({ models: { product: Model, }, routes() { this.namespace = 'api'; this.get('products', ({ products }, request) => { return products.all(); }); }, }); }); afterEach(() => { server.shutdown(); }); it('shows the products', function() { cy.visit('/'); cy.get('li.product').should('have.length', 5); });
ملاحظة : يمكنك دائمًا إلقاء نظرة خاطفة على الأجزاء السابقة من هذه السلسلة إذا كنت لا تفهم أجزاء Mirage من مقتطف الشفرة أعلاه.
- الجزء 1: فهم نماذج وجمعيات Mirage JS
- الجزء الثاني: فهم المصانع والتركيبات والمسلسلات
- الجزء 3: فهم التوقيت والاستجابة والعبور
حسنًا ، لقد بدأنا في تجسيد مثيل الخادم الخاص بنا من خلال إنشاء نموذج المنتج وأيضًا عن طريق إنشاء معالج المسار للمسار /api/products
. ومع ذلك ، إذا أجرينا اختباراتنا ، فسوف تفشل لأنه ليس لدينا أي منتجات في قاعدة بيانات Mirage حتى الآن.
دعنا نملأ قاعدة بيانات Mirage ببعض المنتجات. للقيام بذلك ، كان بإمكاننا استخدام طريقة create()
في مثيل الخادم الخاص بنا ، ولكن إنشاء 5 منتجات يدويًا يبدو مملاً للغاية. يجب أن يكون هناك طريقة أفضل.
آه نعم ، هناك. دعنا نستخدم المصانع (كما هو موضح في الجزء الثاني من هذه السلسلة). سنحتاج إلى إنشاء مصنع منتجاتنا مثل:
// homepage.test.js import { Server, Model, Factory } from 'miragejs'; let server; beforeEach(() => { server = new Server({ models: { product: Model, }, factories: { product: Factory.extend({ name(i) { return `Product ${i}` } }) }, routes() { this.namespace = 'api'; this.get('products', ({ products }, request) => { return products.all(); }); }, }); }); afterEach(() => { server.shutdown(); }); it('shows the products', function() { cy.visit('/'); cy.get('li.product').should('have.length', 5); });
بعد ذلك ، أخيرًا ، سنستخدم createList()
لإنشاء المنتجات الخمسة التي يحتاج اختبارنا لاجتيازها بسرعة.
هيا بنا نقوم بذلك:
// homepage.test.js import { Server, Model, Factory } from 'miragejs'; let server; beforeEach(() => { server = new Server({ models: { product: Model, }, factories: { product: Factory.extend({ name(i) { return `Product ${i}` } }) }, routes() { this.namespace = 'api'; this.get('products', ({ products }, request) => { return products.all(); }); }, }); }); afterEach(() => { server.shutdown(); }); it('shows the products', function() { server.createList("product", 5) cy.visit('/'); cy.get('li.product').should('have.length', 5); });
لذلك عندما نجري اختبارنا ، فإنه يمر!
ملاحظة : بعد كل اختبار ، يتم إغلاق خادم Mirage وإعادة تعيينه ، لذلك لن تتسرب أي من هذه الحالة عبر الاختبارات.
تجنب تعدد خادم ميراج
إذا كنت تتابع هذه السلسلة ، فستلاحظ متى كنا نستخدم Mirage قيد التطوير لاعتراض طلبات شبكتنا ؛ كان لدينا ملف server.js
في جذر تطبيقنا حيث أنشأنا Mirage. بروح DRY (لا تكرر نفسك) ، أعتقد أنه سيكون من الجيد استخدام مثيل الخادم هذا بدلاً من وجود حالتين منفصلتين من Mirage للتطوير والاختبار. للقيام بذلك (في حالة عدم وجود ملف server.js
بالفعل) ، ما عليك سوى إنشاء واحد في دليل src الخاص بالمشروع.
ملاحظة : ستختلف البنية الخاصة بك إذا كنت تستخدم إطار عمل JavaScript ولكن الفكرة العامة هي إعداد ملف server.js في جذر src الخاص بمشروعك.
لذلك مع هذه البنية الجديدة ، سنصدر دالة في server.js
تكون مسؤولة عن إنشاء مثيل خادم Mirage الخاص بنا. لنفعل ذلك:
// src/server.js export function makeServer() { /* Mirage code goes here */}
دعنا نكمل تنفيذ وظيفة makeServer
عن طريق إزالة خادم Mirage JS الذي أنشأناه في homepage.test.js
وإضافته إلى هيكل وظيفة makeServer
:
import { Server, Model, Factory } from 'miragejs'; export function makeServer() { let server = new Server({ models: { product: Model, }, factories: { product: Factory.extend({ name(i) { return `Product ${i}`; }, }), }, routes() { this.namespace = 'api'; this.get('/products', ({ products }) => { return products.all(); }); }, seeds(server) { server.createList('product', 5); }, }); return server; }
الآن كل ما عليك فعله هو استيراد makeServer
في اختبارك. يعد استخدام مثيل Mirage Server واحدًا أكثر نظافة ؛ وبهذه الطريقة لن تضطر إلى الحفاظ على مثيلين للخادم لكل من بيئات التطوير والاختبار.
بعد استيراد وظيفة makeServer
، يجب أن يبدو اختبارنا الآن على النحو التالي:
import { makeServer } from '/path/to/server'; let server; beforeEach(() => { server = makeServer(); }); afterEach(() => { server.shutdown(); }); it('shows the products', function() { server.createList('product', 5); cy.visit('/'); cy.get('li.product').should('have.length', 5); });
لذلك لدينا الآن خادم Mirage مركزي يخدمنا في كل من التطوير والاختبار. يمكنك أيضًا استخدام وظيفة makeServer
لبدء تطوير Mirage (انظر الجزء الأول من هذه السلسلة).
يجب ألا يجد كود ميراج طريقه إلى الإنتاج. لذلك ، بناءً على إعداد البناء الخاص بك ، ستحتاج فقط إلى بدء تشغيل Mirage أثناء وضع التطوير.
ملاحظة : اقرأ مقالتي حول كيفية إعداد API Mocking مع Mirage و Vue.js لمعرفة كيف فعلت ذلك في Vue حتى تتمكن من النسخ المتماثل في أي إطار عمل للواجهة الأمامية تستخدمه.
بيئة الاختبار
يحتوي Mirage على بيئتين: التطوير (افتراضي) والاختبار . في وضع التطوير ، سيكون لخادم Mirage وقت استجابة افتراضي يبلغ 400 مللي ثانية (يمكنك تخصيصه. راجع المقالة الثالثة من هذه السلسلة لذلك) ، ويسجل جميع استجابات الخادم لوحدة التحكم ، ويحمل بذور التطوير.
ومع ذلك ، في بيئة الاختبار ، لدينا:
- 0 تأخير للحفاظ على سرعة اختباراتنا
- يقوم Mirage بمنع جميع السجلات حتى لا تلوث سجلات CI الخاصة بك
- ستتجاهل Mirage أيضًا وظيفة
seeds()
بحيث يمكن استخدام بيانات البذور الخاصة بك من أجل التطوير فقط ولكن لن تتسرب إلى اختباراتك. هذا يساعد على إبقاء اختباراتك حتمية.
دعنا نحدِّث makeServer
الخاص بنا حتى نتمكن من الاستفادة من بيئة الاختبار. للقيام بذلك ، سنجعله يقبل كائنًا بخيار البيئة (سنقوم بالتطوير افتراضيًا وتجاوزه في اختبارنا). يجب أن يبدو server.js
الآن كما يلي:
// src/server.js import { Server, Model, Factory } from 'miragejs'; export function makeServer({ environment = 'development' } = {}) { let server = new Server({ environment, models: { product: Model, }, factories: { product: Factory.extend({ name(i) { return `Product ${i}`; }, }), }, routes() { this.namespace = 'api'; this.get('/products', ({ products }) => { return products.all(); }); }, seeds(server) { server.createList('product', 5); }, }); return server; }
لاحظ أيضًا أننا نقوم بتمرير خيار البيئة إلى مثيل خادم Mirage باستخدام اختصار الخاصية ES6. الآن مع وجود هذا في مكانه الصحيح ، دعنا نحدث اختبارنا لتجاوز قيمة البيئة للاختبار. يبدو اختبارنا الآن كما يلي:
import { makeServer } from '/path/to/server'; let server; beforeEach(() => { server = makeServer({ environment: 'test' }); }); afterEach(() => { server.shutdown(); }); it('shows the products', function() { server.createList('product', 5); cy.visit('/'); cy.get('li.product').should('have.length', 5); });
اختبار AAA
تشجع Mirage معيارًا للاختبار يسمى نهج الاختبار AAA أو الثلاثي. هذا يعني الترتيب والتصرف والتأكيد . يمكنك أن ترى هذا الهيكل في اختبارنا أعلاه بالفعل:
it("shows all the products", function () { // ARRANGE server.createList("product", 5) // ACT cy.visit("/") // ASSERT cy.get("li.product").should("have.length", 5) })
قد تحتاج إلى كسر هذا النمط ولكن 9 مرات من أصل 10 يجب أن تعمل بشكل جيد لاختباراتك.
دعونا نختبر الأخطاء
حتى الآن ، اختبرنا صفحتنا الرئيسية لمعرفة ما إذا كانت تحتوي على 5 منتجات ، ومع ذلك ، ماذا لو تعطل الخادم أو حدث خطأ ما في جلب المنتجات؟ لا نحتاج إلى انتظار تعطل الخادم للعمل على الشكل الذي ستبدو عليه واجهة المستخدم الخاصة بنا في مثل هذه الحالة. يمكننا ببساطة محاكاة هذا السيناريو مع ميراج.
لنعد 500 (خطأ في الخادم) عندما يكون المستخدم على الصفحة الرئيسية. كما رأينا في مقال سابق ، لتخصيص ردود Mirage ، نستخدم فئة الاستجابة. دعنا نستوردها ونكتب اختبارنا.
homepage.test.js import { Response } from "miragejs" it('shows an error when fetching products fails', function() { server.get('/products', () => { return new Response( 500, {}, { error: "Can't fetch products at this time" } ); }); cy.visit('/'); cy.get('div.error').should('contain', "Can't fetch products at this time"); });
يا له من عالم من المرونة! لقد تجاوزنا الاستجابة التي ستعود بها Mirage من أجل اختبار كيفية عرض واجهة المستخدم الخاصة بنا إذا فشلت في جلب المنتجات. سيبدو ملف homepage.test.js
العام كما يلي:
// homepage.test.js import { Response } from 'miragejs'; import { makeServer } from 'path/to/server'; let server; beforeEach(() => { server = makeServer({ environment: 'test' }); }); afterEach(() => { server.shutdown(); }); it('shows the products', function() { server.createList('product', 5); cy.visit('/'); cy.get('li.product').should('have.length', 5); }); it('shows an error when fetching products fails', function() { server.get('/products', () => { return new Response( 500, {}, { error: "Can't fetch products at this time" } ); }); cy.visit('/'); cy.get('div.error').should('contain', "Can't fetch products at this time"); });
لاحظ التعديل الذي أجريناه على معالج /api/products
فقط في اختبارنا. هذا يعني أنه يعمل كما حددنا سابقًا عندما تكون في وضع التطوير.
لذلك عندما نجري اختباراتنا ، يجب أن يجتاز كلاهما.
ملاحظة : أعتقد أنه من الجدير ملاحظة أن العناصر التي نستفسر عنها في Cypress يجب أن تكون موجودة في واجهة المستخدم الأمامية. لا يقوم Cypress بإنشاء عناصر HTML من أجلك.
اختبار صفحة تفاصيل المنتج
أخيرًا ، دعنا نختبر واجهة المستخدم لصفحة تفاصيل المنتج. إذن هذا ما نختبره من أجله:
- يمكن للمستخدم رؤية اسم المنتج في صفحة تفاصيل المنتج
دعنا نذهب اليها. أولاً ، نقوم بإنشاء اختبار جديد لاختبار تدفق المستخدم هذا.
ها هو الاختبار:
it("shows the product's name on the detail route", function() { let product = this.server.create('product', { name: 'Korg Piano', }); cy.visit(`/${product.id}`); cy.get('h1').should('contain', 'Korg Piano'); });
يجب أن يبدو homepage.test.js
أخيرًا هكذا.
// homepage.test.js import { Response } from 'miragejs'; import { makeServer } from 'path/to/server; let server; beforeEach(() => { server = makeServer({ environment: 'test' }); }); afterEach(() => { server.shutdown(); }); it('shows the products', function() { console.log(server); server.createList('product', 5); cy.visit('/'); cy.get('li.product').should('have.length', 5); }); it('shows an error when fetching products fails', function() { server.get('/products', () => { return new Response( 500, {}, { error: "Can't fetch products at this time" } ); }); cy.visit('/'); cy.get('div.error').should('contain', "Can't fetch products at this time"); }); it("shows the product's name on the detail route", function() { let product = server.create('product', { name: 'Korg Piano', }); cy.visit(`/${product.id}`); cy.get('h1').should('contain', 'Korg Piano'); });
عند إجراء الاختبارات الخاصة بك ، يجب أن يجتاز الثلاثة جميعهم.
تغليف
لقد كان من الممتع أن أريكم أبطال Mirage JS في هذه السلسلة. أتمنى أن تكون مجهزًا بشكل أفضل لبدء تجربة تطوير أفضل للواجهة الأمامية باستخدام Mirage للاستهزاء بخادمك الخلفي. آمل أيضًا أن تستخدم المعرفة من هذه المقالة لكتابة المزيد من اختبارات القبول / واجهة المستخدم / الاختبارات الشاملة لتطبيقاتك الأمامية.
- الجزء 1: فهم نماذج وجمعيات Mirage JS
- الجزء الثاني: فهم المصانع والتركيبات والمسلسلات
- الجزء 3: فهم التوقيت والاستجابة والعبور
- الجزء 4: استخدام Mirage JS و Cypress لاختبار واجهة المستخدم