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

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

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

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

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

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 داخل دليل الدوال.

جارٍ إعداد Firebase Test SDK لإجراء Cloud Functions

هناك طريقتان لاستخدام firebase-functions-test:

  1. الوضع على الإنترنت (مُقترَح): يمكنك كتابة اختبارات تتفاعل مع مشروع Firebase مخصّص للاختبار لكي تتم عمليات الكتابة في قاعدة البيانات وإنشاء المستخدمين وما إلى ذلك، ويمكن لرمز الاختبار فحص النتائج. ويعني ذلك أيضًا أنّه سيتمكّن غيرها من حِزم تطوير البرامج (SDK) من Google المستخدَمة في وظائفك من العمل أيضًا.
  2. الوضع بلا إنترنت: يمكنك كتابة اختبارات وحدات منفصلة وبلا إنترنت بدون أيّ تأثيرات جانبية. وهذا يعني أنّه يجب إنشاء نماذج بديلة لأيّ طلبات أسلوب تتفاعل مع أحد منتجات Firebase (مثل الكتابة في قاعدة البيانات أو إنشاء مستخدم). لا يُنصح بشكل عام باستخدام Cloud Firestore وضع بلا إنترنت إذا كانت لديك وظائف 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() في رمز الدوالّ، يمكنك محاكاة قيم config. على سبيل المثال، إذا كان functions/index.js يحتوي على الرمز التالي:

const functions = require('firebase-functions/v1');
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 طرقًا لإنشاء مثال على data أو بيانات مخصّصة.
  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 لإنشاء DocumentSnapshot في Firestore. الوسيطة الأولى هي البيانات، و الوسيطة الثانية هي مسار المرجع الكامل، وهناك وسيطة ثالثة اختيارية للخصائص الأخرى لللقطة التي يمكنك تحديدها.

// 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 للقيام بهذه الخطوات.

إجراء التأكيدات في الوضع على الإنترنت

إذا تمّت بدء تشغيل Firebase Test SDK لتطبيق Cloud Functions في الوضع على الإنترنت، يمكنك التأكّد من أنّ الإجراءات المطلوبة (مثل كتابة قاعدة بيانات) قد تمّت باستخدام حزمة تطوير البرامج (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 لتأكيد أنّه تمّ استدعاء طرق معيّنة باستخدام المَعلمات التي تتوقّعها.

اختبار وظائف HTTP

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

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

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

تأخذ دالة onRequest في HTTP مَعلمتَين: عنصر طلب وعنصر استجابة. في ما يلي كيفية اختبار مثال الدالة 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);

تنظيف الاختبار

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

test.cleanup();

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

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

لمزيد من المعلومات، يُرجى الرجوع إلى مرجع واجهة برمجة التطبيقات لنظام التشغيل firebase-functions-test.