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

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

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

تنظیم تست

با اجرای دستورات زیر در پوشه توابع خود، هم firebase-functions-test و هم Mocha ، یک چارچوب آزمایشی را نصب کنید:

npm install --save-dev firebase-functions-test
npm install --save-dev mocha

سپس یک پوشه test در داخل پوشه توابع ایجاد کنید، یک فایل جدید در داخل آن برای کد تست خود ایجاد کنید و نام آن را چیزی مانند index.test.js بگذارید.

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

"scripts": {
  "test": "mocha --reporter spec"
}

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

راه اندازی Firebase Test SDK برای توابع Cloud

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

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

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

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

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

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

    برای برنامه های وب، Config را برای نمایش مقادیر پیکربندی انتخاب کنید.

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

  1. صفحه حساب‌های سرویس کنسول Google Cloud را باز کنید.
  2. حساب سرویس پیش‌فرض App Engine را انتخاب کنید و از منوی گزینه‌ها در سمت راست برای انتخاب کلید ایجاد استفاده کنید.
  3. وقتی از شما خواسته شد، JSON را برای نوع کلید انتخاب کنید و روی ایجاد کلیک کنید.

پس از ذخیره فایل کلید، 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');
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. داده (الزامی): داده هایی برای ارسال به 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);

به مرجع 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 یا پایگاه داده بیدرنگ هستید، باید به جای ایجاد یک 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 برای 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);

اظهار نظر

پس از مقداردهی اولیه SDK، بسته بندی توابع، و ساخت داده ها، می توانید توابع پیچیده شده را با داده های ساخته شده فراخوانی کنید و در مورد رفتار اظهار نظر کنید. برای بیان این ادعاها می توانید از کتابخانه ای مانند چای استفاده کنید.

اظهار نظر در حالت آنلاین

اگر Firebase Test SDK برای توابع ابری را در حالت آنلاین راه‌اندازی کرده باشید، می‌توانید با استفاده از firebase-admin SDK ادعا کنید که اقدامات مورد نظر (مانند نوشتن پایگاه داده) انجام شده است.

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

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

  • شما از functions.config() استفاده می کنید
  • عملکرد شما با یک پروژه Firebase یا سایر APIهای Google تعامل دارد و می‌خواهید از یک پروژه 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();

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

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

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