בדף הזה מתוארים כלים ושיטות מומלצות לכתיבת בדיקות יחידה (unit tests) לפונקציות, כמו בדיקות שהן חלק ממערכת שילוב רציף (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:
- מצב אונליין (מומלץ): כותבים בדיקות שיוצרות אינטראקציה עם פרויקט Firebase שמיועד לבדיקות, כדי שפעולות כמו כתיבה במסד הנתונים, יצירת משתמשים וכו' יתרחשו בפועל, וקוד הבדיקה יוכל לבדוק את התוצאות. זה גם אומר שערכות SDK אחרות של Google שבהן אתם משתמשים בפונקציות שלכם יעבדו.
- מצב אופליין: כתיבת בדיקות יחידה מבודדות ואופליין ללא תופעות לוואי. כלומר, צריך להחליף כל הפעלת method שמקיימת אינטראקציה עם מוצר Firebase (למשל, כתיבה למסד הנתונים או יצירת משתמש) ב-stub. בדרך כלל לא מומלץ להשתמש במצב אופליין אם יש לכם פונקציות Cloud Firestore או Realtime Database, כי זה מגדיל מאוד את המורכבות של קוד הבדיקה.
אתחול ה-SDK במצב אונליין (מומלץ)
אם אתם רוצים לכתוב בדיקות שיוצרות אינטראקציה עם פרויקט בדיקה, אתם צריכים לספק את ערכי ההגדרות האישיות של Firebase שנדרשים כדי לאתחל את האפליקציה באמצעות firebase-admin, ואת הנתיב לקובץ מפתח של חשבון שירות.
כדי לקבל את ערכי ההגדרות האישיות של Firebase:
במסוף Firebase, נכנסים לדף
Settings > General.גוללים לכרטיס האפליקציות שלך ובוחרים את האפליקציה הרצויה.
כדי לקבל את הגדרות Firebase:
באפליקציות ל-Apple ול-Android, בוחרים באפשרות להוריד קובץ תצורה.
באפליקציות אינטרנט, בוחרים באפשרות Config (הגדרה) כדי להציג את ערכי ההגדרות.
כדי ליצור קובץ מפתח:
במסוף Google Cloud, נכנסים לחלונית Service Accounts.
בוחרים את חשבון השירות שמוגדר כברירת מחדל של App Engine, ובתפריט האפשרויות שמשמאל בוחרים באפשרות Create key.
כשמופיעה בקשה, בוחרים באפשרות JSON בתור סוג המפתח ולוחצים על Create.
אחרי ששומרים את קובץ המפתח, מפעילים את ה-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() בקוד הפונקציות, תצטרכו להגדיר לו stub לפני שתייבאו את הפונקציות. בקובץ הבדיקה, דורשים את 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:
- עוטפים את הפונקציה שרוצים לבדוק באמצעות השיטה
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. הוא תואם ישירות לפרמטר הראשון שנשלח ל-function handler שכתבתם. ב-firebase-functions-testיש שיטות ליצירת נתונים בהתאמה אישית או נתונים לדוגמה. - eventContextOptions (אופציונלי): שדות של הקשר האירוע שרוצים לציין. הקשר האירוע הוא הפרמטר השני שנשלח ל-function handler שכתבתם. אם לא כוללים פרמטר
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, תצטרכו ליצור שני קובצי Snapshot: אחד למצב שלפני ואחד למצב שאחרי. אחר כך אפשר להשתמש ב-makeChange method כדי ליצור אובייקט 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, אתם צריכים להשתמש באובייקט פשוט עם סטאבים במקום ליצור 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 במצב אונליין, אתם יכולים לאשר שהפעולות הרצויות (כמו כתיבה במסד נתונים) בוצעו באמצעות ה-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. 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
כדי לבדוק פונקציות onCall של HTTP, משתמשים באותה גישה כמו בדיקת פונקציות ברקע.
אם אתם בודקים פונקציות onRequest של HTTP, כדאי להשתמש ב-firebase-functions-test במקרים הבאים:
- אתם משתמשים ב-
functions.config() - הפונקציה שלכם מקיימת אינטראקציה עם פרויקט ב-Firebase או עם Google APIs אחרים, ואתם רוצים להשתמש בפרויקט אמיתי ב-Firebase ובפרטי הכניסה שלו לבדיקות.
פונקציית onRequest של HTTP מקבלת שני פרמטרים: אובייקט בקשה ואובייקט תגובה. כך אפשר לבדוק את הפונקציה לדוגמה addMessage():
Stub
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.
- בדיקת פונקציות Realtime Database ו-HTTP במצב אונליין
- בדיקת פונקציות Realtime Database ו-HTTP במצב אופליין
מידע נוסף זמין במאמרי העזרה של ה-API של firebase-functions-test.