اختبار الوحدة للوظائف السحابية

توضح هذه الصفحة أفضل الممارسات والأدوات لكتابة اختبارات الوحدة لوظائفك، مثل الاختبارات التي قد تكون جزءًا من نظام التكامل المستمر (CI). لتسهيل الاختبار، يوفر Firebase حزمة Firebase Test SDK لوظائف السحابة. يتم توزيعه على 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 داخل دليل الوظائف الخاص بك.

تهيئة Firebase Test SDK للوظائف السحابية

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

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

راجع مرجع واجهة برمجة التطبيقات (API) للحصول على وظائف مماثلة لجميع أنواع البيانات الأخرى.

باستخدام بيانات المثال

إذا لم تكن بحاجة إلى تخصيص البيانات المستخدمة في اختباراتك، فإن 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();

راجع مرجع واجهة برمجة التطبيقات (API) للتعرف على طرق الحصول على بيانات نموذجية لكل نوع وظيفة.

استخدام البيانات المتوقفة (للوضع دون اتصال)

إذا قمت بتهيئة 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 لوظائف السحابة في الوضع عبر الإنترنت ، فيمكنك التأكد من أن الإجراءات المطلوبة (مثل كتابة قاعدة البيانات) قد تم تنفيذها باستخدام 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 assert.equal(wrapped(snap), true);

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

اختبار وظائف 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 .