اختبار وحدة دوال السحابة

تصف هذه الصفحة أفضل الممارسات والأدوات لكتابة اختبارات الوحدة لدوالك، مثل الاختبارات التي قد تكون جزءًا من نظام التكامل المستمر (CI). لتسهيل الاختبار، يوفّر Firebase حزمة تطوير البرامج (SDK) لاختبار Firebase للوظائف السحابية. ويتم توزيعها على npm بتنسيق firebase-functions-test، وهي عبارة عن حزمة SDK اختبارية مصاحبة في firebase-functions. حزمة تطوير البرامج (SDK) الاختبارية لاختبار Firebase لوظائف السحابة الإلكترونية:

  • الاهتمام بالإعداد المناسب والتفاصيل المناسبة للاختبارات، مثل ضبط متغيّرات البيئة وإلغاء ضبطها التي يحتاجها firebase-functions.
  • تنشئ نموذجًا للبيانات وسياق الحدث، لكي يكون عليك فقط تحديد الحقول ذات الصلة باختبارك.

اختبار الإعداد

ثبِّت كل من firebase-functions-test وMocha، وهو إطار عمل للاختبار، من خلال تشغيل الأوامر التالية في مجلد الدوال:

npm install --save-dev firebase-functions-test
npm install --save-dev mocha

بعد ذلك، أنشِئ مجلد test داخل مجلد الدوال، وأنشِئ ملفًا جديدًا داخله لرمز الاختبار، وأدخِل اسمًا له، مثل index.test.js.

أخيرًا، عدِّل functions/package.json لإضافة ما يلي:

"scripts": {
  "test": "mocha --reporter spec"
}

بعد كتابة الاختبارات، يمكنك إجراؤها من خلال تشغيل npm test داخل دليل الدوال.

جارٍ إعداد حزمة تطوير البرامج (SDK) التجريبية لاختبار Firebase لوظائف السحابة الإلكترونية

تتوفّر طريقتان لاستخدام "firebase-functions-test":

  1. وضع الاتصال بالإنترنت (يُنصح به): اكتب اختبارات تتفاعل مع مشروع Firebase ومخصّصة للاختبار حتى تحدث عمليات كتابة قاعدة البيانات وعمليات إنشاء المستخدم وما إلى ذلك، ويمكن لرمز الاختبار الخاص بك فحص النتائج. وهذا يعني أيضًا أن حزم SDK الأخرى من Google المستخدمة في الدوال ستعمل أيضًا.
  2. وضع عدم الاتصال بالإنترنت: يمكنك كتابة اختبارات الوحدات المعزولة وبلا اتصال بالإنترنت بدون أي آثار جانبية. وهذا يعني أن أي استدعاءات طريقة تتفاعل مع أحد منتجات Firebase (على سبيل المثال، الكتابة في قاعدة البيانات أو إنشاء مستخدم) بحاجة إلى دبلجة. لا ننصح عمومًا باستخدام "وضع عدم الاتصال بالإنترنت" إذا كانت لديك وظيفتا Cloud Firestore أو Realtime Database، لأن ذلك يؤدي إلى زيادة درجة تعقيد رمز الاختبار بشكل كبير.

إعداد حزمة تطوير البرامج (SDK) في وضع الاتصال (يُنصح بهذا الخيار)

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

للحصول على قيم الإعدادات لمشروع Firebase:

  1. افتح إعدادات مشروعك في وحدة تحكُّم Firebase.
  2. في تطبيقاتك، اختَر التطبيق المطلوب.
  3. في الجزء الأيسر، حدِّد الخيار لتنزيل ملف إعداد لتطبيقات Apple وAndroid.

    بالنسبة إلى تطبيقات الويب، اختَر الإعداد لعرض قيم الضبط.

لإنشاء ملف مفتاح:

  1. افتح جزء حسابات الخدمة في وحدة التحكّم في Google Cloud.
  2. اختَر حساب خدمة App Engine التلقائي واستخدِم قائمة الخيارات على يسار الصفحة لاختيار إنشاء مفتاح.
  3. عندما يُطلب منك، اختَر JSON لنوع المفتاح، ثم انقر على إنشاء.

بعد حفظ ملف المفتاح، ابدأ إعداد حزمة تطوير البرامج (SDK):

// At the top of test/index.test.js
const test = require('firebase-functions-test')({
  databaseURL: 'https://my-project.firebaseio.com',
  storageBucket: 'my-project.appspot.com',
  projectId: 'my-project',
}, 'path/to/serviceAccountKey.json');

إعداد حزمة تطوير البرامج (SDK) في وضع عدم الاتصال بالإنترنت

إذا أردت كتابة اختبارات بلا اتّصال بالإنترنت بالكامل، يمكنك إعداد حزمة تطوير البرامج (SDK) بدون أي مَعلمات:

// At the top of test/index.test.js
const test = require('firebase-functions-test')();

قيم التكوين الافتراضية

إذا كنت تستخدم functions.config() في رمز الدوال، يمكنك محاكاة قيم الضبط. على سبيل المثال، إذا كان functions/index.js يحتوي على الرمز التالي:

const functions = require('firebase-functions');
const key = functions.config().stripe.key;

بعد ذلك، يمكنك محاكاة القيمة داخل ملف الاختبار على النحو التالي:

// Mock functions config values
test.mockConfig({ stripe: { key: '23wr42ewr34' }});

استيراد الدوال

لاستيراد الدوال، استخدِم require لاستيراد ملف الدوال الرئيسي كوحدة. احرص على عدم تنفيذ هذا الإجراء إلا بعد إعداد firebase-functions-test ومحاكاة قيم الإعدادات.

// after firebase-functions-test has been initialized
const myFunctions = require('../index.js'); // relative path to functions code

في حال إعداد firebase-functions-test في وضع عدم الاتصال بالإنترنت وكان لديك admin.initializeApp() في رمز الدوال، عليك استخدامه قبل استيراد الدوال:

// If index.js calls admin.initializeApp at the top of the file,
// we need to stub it out before requiring index.js. This is because the
// functions will be executed as a part of the require process.
// Here we stub admin.initializeApp to be a dummy function that doesn't do anything.
adminInitStub = sinon.stub(admin, 'initializeApp');
// Now we can require index.js and save the exports inside a namespace called myFunctions.
myFunctions = require('../index');

اختبار دوال الخلفية (التي لا تستخدم HTTP)

تتضمّن عملية اختبار الدوال التي لا تتوافق مع HTTP الخطوات التالية:

  1. التفاف الدالة التي تريد اختبارها باستخدام الطريقة test.wrap
  2. إنشاء بيانات الاختبار
  3. استدعِ الدالة المضمّنة مع بيانات الاختبار التي أنشأتها وأي حقول لسياق الحدث تريد تحديدها.
  4. تقديم تأكيدات حول السلوك

أولاً، قم بلف الدالة التي تريد اختبارها. لنفترض أن لديك دالة في functions/index.js تُسمى makeUppercase، وتريد اختبارها. اكتب ما يلي باللغة functions/test/index.test.js

// "Wrap" the makeUpperCase function from index.js
const myFunctions = require('../index.js');
const wrapped = test.wrap(myFunctions.makeUppercase);

wrapped هي دالة تستدعي makeUppercase عند استدعائها. تأخذ wrapped معلَمتَين:

  1. data (مطلوبة): البيانات المطلوب إرسالها إلى makeUppercase. يتجاوب هذا بشكل مباشر مع المعلمة الأولى التي يتم إرسالها إلى معالج الدالة الذي كتبته. توفر firebase-functions-test طرقًا لإنشاء بيانات مخصّصة أو نماذج بيانات.
  2. eventContextOptions (اختيارية): حقول سياق الحدث التي تريد تحديدها. سياق الحدث هو المعلمة الثانية التي يتم إرسالها إلى معالج الدوال الذي كتبته. في حال عدم تضمين مَعلمة eventContextOptions عند استدعاء wrapped، سيستمر إنشاء سياق حدث باستخدام حقول معقولة. يمكنك تجاوز بعض الحقول التي تم إنشاؤها من خلال تحديدها هنا. لاحظ أنه يجب عليك فقط تضمين الحقول التي ترغب في تجاوزها. يتم إنشاء أي حقول لم يتم تجاوزها.
const data = … // See next section for constructing test data

// Invoke the wrapped function without specifying the event context.
wrapped(data);

// Invoke the function, and specify params
wrapped(data, {
  params: {
    pushId: '234234'
  }
});

// Invoke the function, and specify auth and auth Type (for real time database functions only)
wrapped(data, {
  auth: {
    uid: 'jckS2Q0'
  },
  authType: 'USER'
});

// Invoke the function, and specify all the fields that can be specified
wrapped(data, {
  eventId: 'abc',
  timestamp: '2018-03-23T17:27:17.099Z',
  params: {
    pushId: '234234'
  },
  auth: {
    uid: 'jckS2Q0' // only for real time database functions
  },
  authType: 'USER' // only for real time database functions
});

إنشاء بيانات الاختبار

المعلمة الأولى لدالة ملفوفة هي بيانات الاختبار لاستدعاء الدالة الأساسية معها. هناك عدد من الطرق لإنشاء بيانات الاختبار.

استخدام البيانات المخصّصة

تتضمن الدالة firebase-functions-test عددًا من الدوال لإنشاء البيانات اللازمة لاختبار الدوال. على سبيل المثال، استخدِم test.firestore.makeDocumentSnapshot لإنشاء Firestore DocumentSnapshot. الوسيطة الأولى هي البيانات، والوسيطة الثانية هي المسار المرجعي الكامل، وهناك وسيطة ثالثة اختيارية لخصائص أخرى يمكنك تحديدها في اللقطة.

// Make snapshot
const snap = test.firestore.makeDocumentSnapshot({foo: 'bar'}, 'document/path');
// Call wrapped function with the snapshot
const wrapped = test.wrap(myFunctions.myFirestoreDeleteFunction);
wrapped(snap);

إذا كنت تختبر الدالة onUpdate أو onWrite، عليك إنشاء لقطتَين: إحداهما للحالة السابقة والأخرى للحالة اللاحقة. بعد ذلك، يمكنك استخدام طريقة makeChange لإنشاء كائن Change باستخدام هذه اللقطات.

// Make snapshot for state of database beforehand
const beforeSnap = test.firestore.makeDocumentSnapshot({foo: 'bar'}, 'document/path');
// Make snapshot for state of database after the change
const afterSnap = test.firestore.makeDocumentSnapshot({foo: 'faz'}, 'document/path');
const change = test.makeChange(beforeSnap, afterSnap);
// Call wrapped function with the Change object
const wrapped = test.wrap(myFunctions.myFirestoreUpdateFunction);
wrapped(change);

يمكنك الاطّلاع على مرجع واجهة برمجة التطبيقات للحصول على دوال مشابهة لجميع أنواع البيانات الأخرى.

استخدام نموذج البيانات

إذا لم تكن بحاجة إلى تخصيص البيانات المستخدَمة في اختباراتك، تقدّم لك خدمة "firebase-functions-test" طرقًا لإنشاء نماذج بيانات لكل نوع من أنواع الدوال.

// For Firestore onCreate or onDelete functions
const snap = test.firestore.exampleDocumentSnapshot();
// For Firestore onUpdate or onWrite functions
const change = test.firestore.exampleDocumentSnapshotChange();

اطّلع على مرجع واجهة برمجة التطبيقات للتعرُّف على طرق الحصول على أمثلة على البيانات لكل نوع من الدوال.

استخدام البيانات التي تم استئصالها (لوضع عدم الاتصال بالإنترنت)

إذا أعددت حزمة تطوير البرامج (SDK) في وضع عدم الاتصال بالإنترنت، وكنت تختبر دالة Cloud Firestore أو في الوقت الفعلي Realtime Database، يجب استخدام كائن عادي يحتوي على رموز بديلة بدلاً من إنشاء DocumentSnapshot أو DataSnapshot فعليين.

لنفترض أنك تكتب اختبار وحدة للدالة التالية:

// Listens for new messages added to /messages/:pushId/original and creates an
// uppercase version of the message to /messages/:pushId/uppercase
exports.makeUppercase = functions.database.ref('/messages/{pushId}/original')
    .onCreate((snapshot, context) => {
      // Grab the current value of what was written to the Realtime Database.
      const original = snapshot.val();
      functions.logger.log('Uppercasing', context.params.pushId, original);
      const uppercase = original.toUpperCase();
      // You must return a Promise when performing asynchronous tasks inside a Functions such as
      // writing to the Firebase Realtime Database.
      // Setting an "uppercase" sibling in the Realtime Database returns a Promise.
      return snapshot.ref.parent.child('uppercase').set(uppercase);
    });

داخل الدالة، يتم استخدام snap مرتين:

  • snap.val()
  • snap.ref.parent.child('uppercase').set(uppercase)

في رمز الاختبار، يمكنك إنشاء كائن عادي حيث يعمل كلا مسارَي الرمز، واستخدام Sinon لاستبدال الطرق.

// The following lines creates a fake snapshot, 'snap', which returns 'input' when snap.val() is called,
// and returns true when snap.ref.parent.child('uppercase').set('INPUT') is called.
const snap = {
  val: () => 'input',
  ref: {
    parent: {
      child: childStub,
    }
  }
};
childStub.withArgs(childParam).returns({ set: setStub });
setStub.withArgs(setParam).returns(true);

تقديم التأكيدات

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

تقديم التأكيدات أثناء الاتصال بالإنترنت

إذا أعددت حزمة تطوير البرامج (SDK) لاختبار Firebase للوظائف السحابية في الوضع على الإنترنت، يمكنك تأكيد تنفيذ الإجراءات المطلوبة (مثل كتابة قاعدة البيانات) باستخدام حزمة تطوير البرامج (SDK) firebase-admin.

يؤكد المثال أدناه أن 'INPUT' مكتوبة في قاعدة بيانات مشروع الاختبار.

// Create a DataSnapshot with the value 'input' and the reference path 'messages/11111/original'.
const snap = test.database.makeDataSnapshot('input', 'messages/11111/original');

// Wrap the makeUppercase function
const wrapped = test.wrap(myFunctions.makeUppercase);
// Call the wrapped function with the snapshot you constructed.
return wrapped(snap).then(() => {
  // Read the value of the data at messages/11111/uppercase. Because `admin.initializeApp()` is
  // called in functions/index.js, there's already a Firebase app initialized. Otherwise, add
  // `admin.initializeApp()` before this line.
  return admin.database().ref('messages/11111/uppercase').once('value').then((createdSnap) => {
    // Assert that the value is the uppercased version of our input.
    assert.equal(createdSnap.val(), 'INPUT');
  });
});

تقديم تأكيدات في وضع عدم الاتصال

يمكنك تقديم تأكيدات حول القيمة الناتجة المتوقعة للدالة:

const childParam = 'uppercase';
const setParam = 'INPUT';
// Stubs are objects that fake and/or record function calls.
// These are excellent for verifying that functions have been called and to validate the
// parameters passed to those functions.
const childStub = sinon.stub();
const setStub = sinon.stub();
// The following lines creates a fake snapshot, 'snap', which returns 'input' when snap.val() is called,
// and returns true when snap.ref.parent.child('uppercase').set('INPUT') is called.
const snap = {
  val: () => 'input',
  ref: {
    parent: {
      child: childStub,
    }
  }
};
childStub.withArgs(childParam).returns({ set: setStub });
setStub.withArgs(setParam).returns(true);
// Wrap the makeUppercase function.
const wrapped = test.wrap(myFunctions.makeUppercase);
// Since we've stubbed snap.ref.parent.child(childParam).set(setParam) to return true if it was
// called with the parameters we expect, we assert that it indeed returned true.
return assert.equal(wrapped(snap), true);

ويمكنك أيضًا استخدام Sinon sinon sinon Sinon Sinon sinon sinon sinon sinon sinon sinon sinon) في مغامرة للتأكد من استدعائها للاطّلاع على بعض هذه الطرق باستخدام المَعلمات التي تتوقعها.

اختبار دوال HTTP

لاختبار دوال HTTP onCall، استخدِم الأسلوب نفسه الذي يتبعه اختبار دوال الخلفية.

إذا كنت تختبر دوال HTTP onRequest، يجب استخدام firebase-functions-test في الحالات التالية:

  • أنت تستخدم functions.config().
  • تتفاعل دالتك مع مشروع Firebase أو واجهات Google APIs الأخرى، وترغب في استخدام مشروع Firebase حقيقي وبيانات اعتماده لإجراء اختباراتك.

تأخذ دالة HTTP onRequest معاملين: كائن الطلب وكائن استجابة. إليك طريقة اختبار مثال الدالة addMessage():

  • تجاوز وظيفة إعادة التوجيه في كائن الاستجابة، بما أنّ sendMessage() تستدعيها.
  • ضمن دالة إعادة التوجيه، استخدم chai.assert للمساعدة في التأكيد على المعلمات التي يجب استدعاء دالة إعادة التوجيه بها:
// A fake request object, with req.query.text set to 'input'
const req = { query: {text: 'input'} };
// A fake response object, with a stubbed redirect function which asserts that it is called
// with parameters 303, 'new_ref'.
const res = {
  redirect: (code, url) => {
    assert.equal(code, 303);
    assert.equal(url, 'new_ref');
    done();
  }
};

// Invoke addMessage with our fake request and response objects. This will cause the
// assertions in the response object to be evaluated.
myFunctions.addMessage(req, res);

إزالة البرامج غير المرغوب فيها

في نهاية رمز الاختبار، استدعِ دالة التنظيف. يؤدي ذلك إلى عدم ضبط متغيّرات البيئة التي ضبطتها حزمة SDK عند إعدادها، كما يؤدي إلى حذف تطبيقات Firebase التي يُحتمل أن يكون قد تم إنشاؤها في حال استخدام حزمة تطوير البرامج (SDK) لإنشاء قاعدة بيانات DataSnapshot أو Firestore DocumentSnapshot.

test.cleanup();

مراجعة الأمثلة الكاملة والاطّلاع على مزيد من المعلومات

يمكنك مراجعة الأمثلة الكاملة على مستودع GitHub في Firebase.

لمزيد من المعلومات، يمكنك الاطّلاع على مرجع واجهة برمجة التطبيقات في firebase-functions-test.