آزمون واحد Cloud Functions

این صفحه روال‌های مطلوب و ابزارهای نوشتن آزمون‌های واحد برای توابع را شرح می‌دهد، مانند آزمون‌هایی که بخشی از سیستم «ادغام مداوم» (CI) هستند. برای آسان‌تر کردن آزمایش، Firebase Firebase Test SDK را برای Cloud Functions ارائه می‌دهد. این کیت توسعه نرم‌افزار به‌عنوان firebase-functions-test در npm توزیع می‌شود و کیت توسعه نرم‌افزار آزمایشی همراه برای 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 را در پوشه functions ایجاد کنید، فایل جدیدی در آن برای کد آزمایشی‌تان ایجاد کنید، و نامی مثل index.test.js برای آن انتخاب کنید.

درنهایت، functions/package.json را اصلاح کنید تا موارد زیر اضافه شود:

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

پس‌از نوشتن آزمایش‌ها، می‌توانید آن‌ها را با اجرای npm test در فهرست راهنمای توابع خود اجرا کنید.

درحال مقداردهی اولیه Firebase Test SDK برای Cloud Functions

دو روش برای استفاده از firebase-functions-test وجود دارد:

  1. حالت آنلاین (توصیه‌شده): آزمایش‌هایی بنویسید که با پروژه Firebase اختصاص‌داده‌شده به آزمایش تعامل داشته باشند تا نوشتن پایگاه داده، ایجاد کاربر، و غیره واقعاً اتفاق بیفتد و کد آزمایشی شما بتواند نتایج را بررسی کند. این یعنی سایر کیت‌های توسعه نرم‌افزار Google که در توابع شما استفاده می‌شوند نیز کار خواهند کرد.
  2. حالت آفلاین: آزمون‌های واحد مجزا و آفلاین بدون عوارض جانبی بنویسید. این یعنی هر فراخوانی روشی که با محصول Firebase تعامل دارد (مثلاً نوشتن در پایگاه داده یا ایجاد کاربر) باید به‌صورت ساختگی شبیه‌سازی شود. اگر توابع Cloud Firestore یا Realtime Database دارید، استفاده از حالت آفلاین معمولاً توصیه نمی‌شود، زیرا پیچیدگی کد آزمایشی شما را به‌شدت افزایش می‌دهد.

مقداردهی اولیه کیت توسعه نرم‌افزار در حالت آنلاین (توصیه می‌شود)

اگر می‌خواهید آزمایش‌هایی بنویسید که با پروژه آزمایشی تعامل داشته باشند، باید مقادیر پیکربندی 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');

مقداردهی اولیه کیت توسعه نرم‌افزار در حالت آفلاین

اگر می‌خواهید آزمایش‌های کاملاً آفلاین بنویسید، می‌توانید کیت توسعه نرم‌افزار را بدون هیچ پارامتری مقداردهی اولیه کنید:

// At the top of test/index.test.js
const test = require('firebase-functions-test')();

مقادیر پیکربندی Mocking

اگر از 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. data (الزامی): داده‌هایی که باید به makeUppercase ارسال شود. این مستقیماً با اولین پارامتر ارسال‌شده به گرداننده تابعی که نوشتید مطابقت دارد. firebase-functions-test روش‌هایی برای ساختن داده‌های سفارشی یا داده‌های نمونه ارائه می‌دهد.
  2. eventContextOptions (اختیاری): فیلدهای بافت رویداد که می‌خواهید مشخص کنید. بافت رویداد دومین پارامتر ارسال‌شده به گرداننده تابع است که شما نوشته‌اید. اگر هنگام فراخوانی wrapped، پارامتر eventContextOptions را اضافه نکنید، بافت رویداد همچنان با فیلدهای معقول تولید می‌شود. می‌توانید برخی‌از فیلدهای تولیدشده را با مشخص کردن آن‌ها در اینجا لغو کنید. توجه داشته باشید که فقط باید فیلدهایی را اضافه کنید که می‌خواهید لغو کنید. هر فیلدی که لغو نکرده‌اید تولید می‌شود.
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);

برای توابع مشابه برای همه انواع داده‌های دیگر، به مرجع میانای برنامه‌سازی کاربردی مراجعه کنید.

استفاده از داده‌های نمونه

اگر نیازی به سفارشی‌سازی داده‌های مورداستفاده در آزمایش‌هایتان ندارید، 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();

برای روش‌های دریافت داده‌های نمونه برای هر نوع تابع، مرجع میانای برنامه‌سازی کاربردی را ببینید.

استفاده از داده‌های ساختگی (برای حالت آفلاین)

اگر کیت توسعه نرم‌افزار را در حالت آفلاین مقداردهی اولیه کرده‌اید و درحال آزمایش تابع 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,
    }
  }
};

ادعا کردن

پس‌از مقداردهی اولیه کیت توسعه نرم‌افزار، بسته‌بندی توابع، و ساخت داده‌ها، می‌توانید توابع بسته‌بندی‌شده را با داده‌های ساخته‌شده فراخوانی کنید و درباره رفتار ادعا کنید. می‌توانید از 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، از همان رویکرد آزمایش کردن توابع پس‌زمینه استفاده کنید.

اگر توابع onRequest اچ‌تی‌تی‌پی را آزمایش می‌کنید، باید از firebase-functions-test استفاده کنید اگر:

  • از functions.config() استفاده می‌کنید
  • تابع شما با پروژه Firebase یا Google APIs دیگر تعامل دارد و می‌خواهید از پروژه Firebase واقعی و اطلاعات کاربری آن برای آزمایش‌هایتان استفاده کنید.

تابع onRequest اچ‌تی‌تی‌پی دو پارامتر می‌گیرد: یک شیء درخواست و یک شیء پاسخ. در اینجا نحوه آزمایش addMessage()تابع نمونه آورده شده است:

  • Stub admin.database()، زیرا addMessage() به Realtime Database ارسال می‌کند. در حالت آفلاین، initializeApp() را به‌صورت ساختگی ایجاد کردید، بنابراین برنامه Firebase برای استفاده از Admin 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);

پاک‌سازی آزمایش

در پایان کد آزمایشی، تابع پاک‌سازی را فراخوانی کنید. این کار متغیرهای محیطی را که کیت توسعه نرم‌افزار هنگام مقداردهی اولیه تنظیم کرده است لغو می‌کند و برنامه‌های Firebase را که ممکن است درصورت استفاده از کیت توسعه نرم‌افزار برای ایجاد پایگاه داده هم‌زمان DataSnapshot یا Firestore DocumentSnapshot ایجاد شده باشند حذف می‌کند.

test.cleanup();

نمونه‌های کامل را مرور کنید و بیشتر بدانید

می‌توانید نمونه‌های کامل را در مخزن Firebase GitHub مرور کنید.

برای کسب اطلاعات بیشتر، به مرجع میانای برنامه‌سازی کاربردی برای firebase-functions-test مراجعه کنید.