בדף הזה מתוארות שיטות מומלצות וכלים לכתיבה של בדיקות יחידה לפונקציות, כמו בדיקות שיהיו חלק ממערכת של שילוב רצוף (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
:
- מצב אונליין (מומלץ): כותבים בדיקות שמקיימות אינטראקציה עם פרויקט Firebase שמיועד לבדיקה, כדי שהפעולות של כתיבת מסדי נתונים, יצירת משתמשים וכו' יתבצעו בפועל, וקוד הבדיקה יוכל לבדוק את התוצאות. המשמעות היא שגם ערכות SDK אחרות של Google שנעשה בהן שימוש בפונקציות יפעלו.
- מצב אופליין: כתיבת בדיקות יחידה מבודדות ואופליין ללא תופעות לוואי. כלומר, צריך ליצור סטאב לכל קריאות שיטות שמקיימות אינטראקציה עם מוצר של Firebase (למשל, כתיבה במסד הנתונים או יצירת משתמש). בדרך כלל לא מומלץ להשתמש במצב אופליין אם יש לכם פונקציות Cloud Firestore או Realtime Database, כי הוא מגביר מאוד את המורכבות של קוד הבדיקה.
איך מפעילים את ה-SDK במצב אונליין (מומלץ)
אם רוצים לכתוב בדיקות שמקיימות אינטראקציה עם פרויקט בדיקה, צריך לספק את ערכי התצורה של הפרויקט שנדרשים כדי לאתחל את האפליקציה דרך firebase-admin
, ואת הנתיב לקובץ מפתח של חשבון שירות.
כדי לקבל את ערכי התצורה של פרויקט Firebase:
- פותחים את הגדרות הפרויקט במסוף Firebase.
- בקטע האפליקציות שלך,בוחרים את האפליקציה הרצויה.
בחלונית השמאלית, בוחרים באפשרות להורדת קובץ תצורה לאפליקציות ל-Apple ול-Android.
באפליקציות אינטרנט, בוחרים באפשרות Config כדי להציג את ערכי ההגדרה.
כדי ליצור קובץ מַפְתח:
- פותחים את החלונית Service Accounts במסוף Google Cloud.
- בוחרים את חשבון השירות שמוגדר כברירת מחדל, App Engine, ובתפריט האפשרויות שבצד שמאל בוחרים באפשרות Create key.
- כשמופיעה בקשה, בוחרים באפשרות 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 כולל את השלבים הבאים:
- עוטפים את הפונקציה שרוצים לבדוק בשיטה
test.wrap
- יצירה של נתוני בדיקה
- מפעילים את הפונקציה הארוזת עם נתוני הבדיקה שיצרתם וכל שדות ההקשר של האירוע שרוצים לציין.
- להצהיר על התנהגות.
קודם צריך לעטוף את הפונקציה שרוצים לבדוק. נניח שיש לכם פונקציה ב-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 פרמטרים:
- data (חובה): הנתונים שרוצים לשלוח אל
makeUppercase
. הוא תואם ישירות לפרמטר הראשון שנשלח למטפל הפונקציה שכתבתם.firebase-functions-test
מספק שיטות ליצירת נתונים מותאמים אישית או נתוני דוגמה. - 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: אחד למצב הקודם ואחד למצב הבא. לאחר מכן תוכלו להשתמש ב-method 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, עוטפים את הפונקציות ובונים נתונים, אפשר להפעיל את הפונקציות העטופות עם הנתונים שנוצרו ולבצע טענות נכוֹנוּת לגבי ההתנהגות. אפשר להשתמש בספרייה כמו 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'); }); });
הצהרות נכוֹנוּת במצב אופליין
אפשר להצהיר על ערך ההחזרה הצפוי של הפונקציה:
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 אמיתי ובפרטי הכניסה שלו לצורך הבדיקות.
פונקציית onRequest של HTTP מקבלת שני פרמטרים: אובייקט בקשה ואובייקט תגובה. כך אפשר לבדוק את הפונקציה לדוגמה 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.
- בדיקת פונקציות Realtime Database ו-HTTP במצב אונליין
- בדיקת פונקציות Realtime Database ו-HTTP במצב אופליין
מידע נוסף זמין בחומר העזרה של ה-API של firebase-functions-test
.