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

تصف هذه الصفحة أفضل الممارسات والأدوات لكتابة اختبارات الوحدة لوظائفك، مثل الاختبارات التي ستكون جزءًا من نظام التكامل المستمر (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، وهو إطار اختبار، عن طريق تشغيل الأوامر التالية في مجلد الوظائف:

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 أو Realtime Database وظائف، لأنّ ذلك يزيد بشكل كبير من تعقيد رمز الاختبار.

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

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

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

  1. في وحدة تحكّم Firebase، انتقِل إلى صفحة الإعدادات > عام.

  2. انتقِل إلى بطاقة تطبيقاتك واختَر التطبيق المطلوب.

  3. احصل على إعداد Firebase:

    • بالنسبة إلى تطبيقات iOS وAndroid، اختَر خيار تنزيل ملف إعداد.

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

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

  1. في وحدة تحكّم Google Cloud، انتقِل إلى لوحة حسابات الخدمة.

  2. اختَر حساب الخدمة التلقائي App Engine، واستخدِم قائمة الخيارات على يسار الشاشة لاختيار إنشاء مفتاح.

  3. عندما يُطلب منك ذلك، اختَر JSON لنوع المفتاح وانقر على إنشاء.

بعد حفظ ملف المفتاح، عليك إعداد حزمة SDK:

// At the top of test/index.test.js
// Make sure to use values from your actual Firebase configuration
const test = require('firebase-functions-test')({
  databaseURL: 'https://PROJECT_ID.firebaseio.com',
  storageBucket: 'PROJECT_ID.firebasestorage.app',
  projectId: 'PROJECT_ID',
}, '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/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. البيانات (مطلوبة): هي البيانات التي سيتم إرسالها إلى 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 لإنشاء 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 في وضع الاتصال بالإنترنت، يمكنك التأكّد من أنّ الإجراءات المطلوبة (مثل الكتابة في قاعدة البيانات) قد تم تنفيذها باستخدام حزمة firebase-admin SDK.

يؤكّد المثال أدناه أنّه تمّت كتابة "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 wrapped(snap).then(makeUppercaseResult => {
  return assert.equal(makeUppercaseResult, true);
});

يمكنك أيضًا استخدام Sinon spies للتأكّد من أنّه تم استدعاء طرق معيّنة، وبالمَعلمات التي تتوقّعها.

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

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

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

  • إذا كنت تستخدم functions.config()
  • إذا كانت وظيفتك تتفاعل مع مشروع Firebase أو واجهات برمجة تطبيقات Google الأخرى، وأردت استخدام مشروع 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 في Realtime Database أو DocumentSnapshot في Firestore.

test.cleanup();

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

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

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