בדיקות יחידה (unit testing) של Cloud Functions

בדף הזה מתוארות שיטות מומלצות וכלים לכתיבת בדיקות יחידה לפונקציות שלכם, כמו בדיקות שהן חלק ממערכת אינטגרציה רציפה (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 בתוך התיקייה functions, יוצרים בה קובץ חדש לקוד הבדיקה ומעניקים לו שם כמו 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-admin, ואת הנתיב לקובץ מפתח של חשבון שירות.

כדי לקבל את ערכי התצורה של פרויקט Firebase:

  1. פותחים את הגדרות הפרויקט במסוף Firebase.
  2. בקטע האפליקציות שלך,בוחרים את האפליקציה הרצויה.
  3. בחלונית השמאלית, בוחרים באפשרות להורדת קובץ תצורה לאפליקציות ל-Apple ול-Android.

    באפליקציות אינטרנט, בוחרים באפשרות Config כדי להציג את ערכי התצורה.

כדי ליצור קובץ מַפְתח:

  1. פותחים את החלונית Service Accounts במסוף Google Cloud.
  2. בוחרים את חשבון השירות שמשמש כברירת מחדל בשביל App Engine ומשתמשים בתפריט האפשרויות שמשמאל כדי לבחור Create key.
  3. כשמופיעה בקשה, בוחרים באפשרות JSON בתור סוג המפתח ולוחצים על Create.

אחרי שמירת קובץ המפתח, מאתחלים את ה-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/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 צריך לקבל 2 פרמטרים:

  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 כדי ליצור DocumentSnapshot ב-Firestore. הארגומנט הראשון הוא הנתונים, והארגומנט השני הוא נתיב ההפניה המלא, ויש ארגומנט שלישי אופציונלי למאפיינים אחרים של קובץ ה-snapshot.

// 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, תצטרכו ליצור שני קובצי snapshot: אחד למצב הקודם ואחד למצב הבא. לאחר מכן תוכלו להשתמש בשיטה makeChange כדי ליצור אובייקט Change עם קובצי ה-snapshot.

// 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, כדאי להשתמש באובייקט רגיל עם stubs במקום ליצור 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, עטיפת הפונקציות ובניית הנתונים, תוכלו להפעיל את הפונקציות הארוזות עם הנתונים המובנים ולטעון טענות נכונות (assertions) לגבי ההתנהגות. אפשר להשתמש בספרייה כמו Chai כדי לבצע את ההצהרות האלה.

הצהרות נכוֹנוּת (assertions) במצב אונליין

אם אתם מאתחלים את 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');
  });
});

טענות נכוֹנוּת (assertion) במצב אופליין

אפשר לטעון טענות לגבי הערך המוחזר הצפוי של הפונקציה:

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, צריך להשתמש באותה גישה שמשמשת לבדיקת פונקציות ברקע.

אם בודקים פונקציות onRequest של HTTP, צריך להשתמש ב-firebase-functions-test אם:

  • השתמשת ב-functions.config()
  • הפונקציה שלכם פועלת בשילוב עם פרויקט Firebase או עם ממשקי Google API אחרים, ואתם רוצים להשתמש בפרויקט 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();

דוגמאות מלאות ומידע נוסף

אפשר לעיין בדוגמאות המלאות במאגר GitHub של Firebase.

מידע נוסף זמין בחומר העזרה של ה-API של firebase-functions-test.