تست واحد توابع ابری

این صفحه بهترین شیوه‌ها و ابزارها را برای نوشتن تست‌های واحد برای توابع شما، مانند تست‌هایی که بخشی از یک سیستم ادغام مداوم (CI) هستند، شرح می‌دهد. برای آسان‌تر کردن تست، Firebase Firebase Test SDK برای Cloud Functions ارائه می‌دهد. این کیت در npm به عنوان firebase-functions-test توزیع شده است و یک کیت توسعه نرم‌افزار همراه برای firebase-functions است. Firebase Test SDK برای Cloud Functions :

  • تنظیمات و بررسی‌های مناسب برای تست‌های شما، مانند تنظیم و حذف متغیرهای محیطی مورد نیاز firebase-functions را انجام می‌دهد.
  • داده‌های نمونه و زمینه رویداد را تولید می‌کند، به طوری که شما فقط باید فیلدهایی را که مربوط به آزمون شما هستند مشخص کنید.

تنظیمات تست

با اجرای دستورات زیر در پوشه 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 در دایرکتوری functions خود اجرا کنید.

مقداردهی اولیه کیت توسعه Firebase Test SDK برای Cloud Functions

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

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

مقداردهی اولیه SDK در حالت آنلاین (توصیه می‌شود)

اگر می‌خواهید تست‌هایی بنویسید که با یک پروژه آزمایشی تعامل داشته باشند، باید مقادیر پیکربندی پروژه مورد نیاز برای مقداردهی اولیه برنامه را از طریق firebase-admin و مسیر فایل کلید حساب سرویس را ارائه دهید.

برای دریافت مقادیر پیکربندی پروژه Firebase خود:

  1. تنظیمات پروژه خود را در کنسول Firebase باز کنید.
  2. در قسمت Your apps، برنامه مورد نظر را انتخاب کنید.
  3. در پنل سمت راست، گزینه دانلود فایل پیکربندی برای برنامه‌های اپل و اندروید را انتخاب کنید.

    برای برنامه‌های وب، برای نمایش مقادیر پیکربندی، گزینه پیکربندی (Config) را انتخاب کنید.

برای ایجاد یک فایل کلیدی:

  1. پنل حساب‌های سرویس (Service Accounts) کنسول Google Cloud را باز کنید.
  2. حساب سرویس پیش‌فرض App Engine را انتخاب کنید و از منوی گزینه‌ها در سمت راست، گزینه Create key را انتخاب کنید.
  3. وقتی از شما خواسته شد، نوع کلید را 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 کنید:

// 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/index.js دارید که می‌خواهید آن را تست کنید. کد زیر را در 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 ارسال شوند. این مستقیماً با اولین پارامتر ارسالی به تابع handler که شما نوشته‌اید، مطابقت دارد. 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 استفاده کنید. آرگومان اول داده‌ها و آرگومان دوم مسیر مرجع کامل است و یک آرگومان سوم اختیاری برای سایر ویژگی‌های 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 هستید، باید به جای ایجاد یک DocumentSnapshot یا DataSnapshot واقعی، از یک شیء ساده با stubها استفاده کنید.

فرض کنید شما در حال نوشتن یک تست واحد برای تابع زیر هستید:

// 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 برای Cloud Functions را در حالت آنلاین مقداردهی اولیه کرده باشید، می‌توانید با استفاده از کیت توسعه نرم‌افزار firebase-admin ادعا کنید که اقدامات مورد نظر (مانند نوشتن پایگاه داده) انجام شده است.

مثال زیر ادعا می‌کند که «ورودی» در پایگاه داده پروژه آزمایشی نوشته شده است.

// 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');
  });
});

ایجاد ادعاها در حالت آفلاین

می‌توانید در مورد مقدار بازگشتی مورد انتظار از تابع، ادعاهایی (assertions) مطرح کنید:

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 wrapped(snap).then(makeUppercaseResult => {
  return assert.equal(makeUppercaseResult, true);
});

همچنین می‌توانید از Sinon spys برای تأیید فراخوانی متدهای خاص و با پارامترهای مورد انتظار خود استفاده کنید.

تست توابع HTTP

برای تست توابع HTTP onCall، از همان رویکرد تست توابع پس‌زمینه استفاده کنید.

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

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

یک تابع HTTP onRequest دو پارامتر می‌گیرد: یک شیء درخواست (request object) و یک شیء پاسخ (response object). در اینجا نحوه‌ی تست تابع مثال addMessage() مشاهده می‌کنید:

  • تابع redirect را در شیء response بازنویسی کنید، زیرا sendMessage() آن را فراخوانی می‌کند.
  • درون تابع redirect، از chai.assert برای کمک به ایجاد ادعاهایی در مورد پارامترهایی که تابع redirect باید با آنها فراخوانی شود، استفاده کنید:
// 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();

مثال‌های کامل را مرور کنید و اطلاعات بیشتری کسب کنید

می‌توانید مثال‌های کامل را در مخزن گیت‌هاب فایربیس مرور کنید.

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