การทดสอบ 1 หน่วยของ Cloud Functions

หน้านี้จะอธิบายถึงแนวทางปฏิบัติที่ดีที่สุดและเครื่องมือในการเขียน การทดสอบ 1 หน่วยสำหรับ เช่น การทดสอบที่จะเป็นส่วนหนึ่งของการรวมอย่างต่อเนื่อง (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 ภายในโฟลเดอร์ฟังก์ชัน ให้สร้างไฟล์ใหม่ ในโค้ดทดสอบ และตั้งชื่อว่า index.test.js

สุดท้าย ให้แก้ไข functions/package.json เพื่อเพิ่มข้อมูลต่อไปนี้

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

เมื่อเขียนการทดสอบแล้ว คุณสามารถดำเนินการทดสอบได้โดยเรียกใช้ npm test ภายใน ไดเรกทอรีฟังก์ชัน

กำลังเริ่มต้น Firebase Test SDK สำหรับ Cloud Functions

คุณใช้ firebase-functions-test ได้ 2 วิธีดังนี้

  1. โหมดออนไลน์ (แนะนำ): เขียนการทดสอบที่โต้ตอบกับโปรเจ็กต์ Firebase เฉพาะสำหรับการทดสอบเพื่อให้เขียนฐานข้อมูล สร้างโดยผู้ใช้ และอื่นๆ และโค้ดทดสอบจะตรวจสอบผลลัพธ์ได้ และยังหมายความว่า Google SDK ที่ใช้ในฟังก์ชันของคุณก็จะทํางานได้เช่นกัน
  2. โหมดออฟไลน์: เขียนการทดสอบหน่วยแบบแยกส่วนและออฟไลน์โดยไม่มีผลข้างเคียง ซึ่งหมายความว่าการเรียกใช้ทุกเมธอดที่โต้ตอบกับผลิตภัณฑ์ Firebase (เช่น เขียนไปยังฐานข้อมูลหรือสร้างผู้ใช้) ต้องตัดออก ใช้งานแบบออฟไลน์ โดยทั่วไปเราไม่แนะนำให้ใช้โหมดนี้หากคุณมี Cloud Firestore หรือ Realtime Database เนื่องจากจะเพิ่มความซับซ้อนของโค้ดทดสอบเป็นอย่างมาก

เริ่มต้น SDK ในโหมดออนไลน์ (แนะนำ)

หากต้องการเขียนการทดสอบที่โต้ตอบกับโปรเจ็กต์ทดสอบ คุณจะต้อง ระบุค่าการกำหนดค่าโปรเจ็กต์ที่จำเป็นสำหรับการเริ่มต้นแอปผ่าน firebase-admin และเส้นทางไปยังไฟล์คีย์บัญชีบริการ

วิธีรับค่าการกําหนดค่าของโปรเจ็กต์ Firebase

  1. เปิดการตั้งค่าโปรเจ็กต์ใน คอนโซล Firebase
  2. ในแอปของคุณ ให้เลือกแอปที่ต้องการ
  3. ในแผงด้านขวา ให้เลือกตัวเลือกเพื่อดาวน์โหลดไฟล์การกำหนดค่า สำหรับแอป Apple และ Android

    สําหรับเว็บแอป ให้เลือกกําหนดค่าเพื่อ���สดง การกำหนดค่า

วิธีสร้างไฟล์คีย์

  1. เปิดแผงบัญชีบริการ ของ Google Cloud Console
  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. data (ต้องระบุ): ข้อมูลที่จะส่งไปที่ makeUppercase ขั้นตอนนี้โดยตรง สอดคล้องกับพารามิเตอร์แรกที่ส่งไปยังเครื่องจัดการฟังก์ชันที่คุณ เขียนไว้ firebase-functions-test ระบุวิธีสร้าง หรือข้อมูลตัวอย่าง
  2. eventContextOptions (ไม่บังคับ): ช่องบริบทเหตุการณ์ที่คุณต้องการ ที่ต้องการระบุ บริบทเหตุการณ์คือพารามิเตอร์ที่ 2 ที่ส่งไปยัง เครื่องจัดการฟังก์ชันที่คุณเขียนไว้ หากคุณไม่มี 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 เพื่อสร้าง Firestore DocumentSnapshot อาร์กิวเมนต์แรกคือข้อมูล และ อาร์กิวเมนต์ที่สองคือเส้นทางการอ้างอิงแบบเต็ม และมี อาร์กิวเมนต์ที่ 3 (ไม่บังคับ) สำหรับพร็อพเพอร์ตี้อื่นๆ ของสแนปชอตที่คุณระบุได้

// 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 คุณจะต้องสร้าง สแนปชอต 2 รายการ โดยรายการหนึ่งสำหรับสถานะก่อนและอีกครั้งสำหรับสถานะหลัง จากนั้น คุณ สามารถใช้เมธอด 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 จริง

สมมติว่าคุณกำลังเขียนการทดสอบ 1 หน่วยสำหรับฟังก์ชันต่อไปนี้

// 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 2 ครั้ง

  • 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 สำหรับฟังก์ชันระบบคลาวด์ในโหมดออนไลน์ คุณจะได้รับ สามารถยืนยันว่าการดำเนินการที่ต้องการ (เช่น การเขียนฐานข้อมูล) นั้นเกิดขึ้นโดย โดยใช้ 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 หรือ Google API อื่นๆ และ คุณต้องการใช้โปรเจ็กต์ Firebase จริงและข้อมูลเข้าสู่ระบบของโปรเจ็กต์สำหรับการทดสอบ

ฟังก์ชัน HTTP onRequest ใช้พารามิเตอร์ 2 ตัว ได้แก่ ออบเจ็กต์คำขอและการตอบกลับ ออบเจ็กต์ ต่อไปนี้คือวิธีทดสอบ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