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

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

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

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

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

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

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

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

"scripts": {
  "test": "jest"
}

بعد كتابة الاختبارات، يمكنك تشغيلها من خلال تنفيذ 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:

    • بالنسبة إلى تطبيقات Apple و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() في الرمز البرمجي للوظائف، عليك إنشاء رمز بديل منه قبل استيراد الوظائف. في ملف الاختبار، اطلب firebase-admin على النحو التالي: admin، ثم استبدِل initializeApp() بـ Jest mock:

// If initializeApp() is called in index.js, we mock it out before requiring index.js
adminInitStub = jest.spyOn(admin, 'initializeApp').mockImplementation(() => {});
// 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)

في رمز الاختبار، أنشئ عنصرًا عاديًا يعمل فيه كلا مسارَي الرمز، واستخدِم وظائف المحاكاة في Jest لإنشاء نسخة تجريبية من الطرق.

// The following lines create 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,
    }
  }
};

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

بعد تهيئة حزمة SDK وتضمين الدوال وإنشاء البيانات، يمكنك استدعاء الدوال المضمّنة باستخدام البيانات التي تم إنشاؤها وإجراء تأكيدات بشأن السلوك. يمكنك استخدام expect في Jest لإجراء عمليات التأكيد هذه.

تقديم تأكيدات في الوضع "على الإنترنت"

إذا بدأت Firebase Test SDK في Cloud Functions في الوضع على الإنترنت، يمكنك التأكّد من أنّ الإجراءات المطلوبة (مثل الكتابة في قاعدة البيانات) قد تمّت باستخدام حزمة تطوير البرامج 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.
  return admin.database().ref('messages/11111/uppercase').once('value').then((createdSnap) => {
    // Assert that the value is the uppercased version of our input.
    expect(createdSnap.val()).toBe('INPUT');
  });
});

تقديم تأكيدات في وضع عدم الاتصال بالإنترنت

يمكنك تقديم تأكيدات بشأن القيمة المتوقّعة التي تعرضها الدالة:

const childParam = 'uppercase';
const setParam = 'INPUT';
// Spies/mocks 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 setStub = jest.fn().mockImplementation((val) => val === setParam ? true : undefined);
const childStub = jest.fn().mockImplementation((path) => path === childParam ? { set: setStub } : undefined);
// The following lines create 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,
    }
  }
};
// Wrap the makeUppercase function.
const wrapped = test.wrap(myFunctions.makeUppercase);
// Since we've mocked 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 => {
  expect(makeUppercaseResult).toBe(true);
});

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

اختبار دوال HTTP

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

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

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

تتلقّى دالة HTTP onRequest مَعلمتَين: عنصر طلب وعنصر استجابة. في ما يلي كيفية اختبار دالة المثال addMessage():

  • رمز بديل admin.database()، لأنّ addMessage() يرسل البيانات إلى قاعدة بيانات الوقت الفعلي. في وضع عدم الاتصال بالإنترنت، تم إنشاء نسخة وهمية من initializeApp()، وبالتالي لا يتوفّر تطبيق Firebase يمكن لمدير SDK استخدامه، كما أنّ admin.database() الحقيقي يتعذّر تنفيذه ويظهر الخطأ The default Firebase app does not exist:

    const pushStub = jest.fn().mockResolvedValue({ ref: 'new_ref' });
    const refStub = jest.fn().mockReturnValue({ push: pushStub });
    Object.defineProperty(admin, 'database', {
      configurable: true,
      writable: true,
      value: jest.fn().mockReturnValue({ ref: refStub }),
    });
    
  • تجاوز دالة إعادة التوجيه في عنصر الاستجابة، لأنّ addMessage() يستدعيها.

  • ضمن دالة إعادة التوجيه، استخدِم expect من Jest لتقديم تأكيدات بشأن المَعلمات التي يجب استدعاء دالة إعادة التوجيه بها:

// 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) => {
    expect(code).toBe(303);
    expect(url).toBe('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.