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

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

إعداد حزمة تطوير البرامج (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 أو في الوقت الفعلي 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 لوظائف السحابة الإلكترونية في وضع الإنترنت، يمكنك التأكيد على أنّ الإجراءات المطلوبة (مثل كتابة قاعدة البيانات) قد تم تنفيذها باستخدام حزمة تطوير البرامج 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 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 أو Firestore DocumentSnapshot في الوقت الفعلي.

test.cleanup();

مراجعة الأمثلة الكاملة ومعرفة المزيد

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

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