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

หน้านี้อธิบายแนวทางปฏิบัติแนะนำและเครื่องมือสำหรับการเขียน Unit Test สำหรับฟังก์ชัน เช่น การทดสอบที่จะเป็นส่วนหนึ่งของระบบการรวมอย่างต่อเนื่อง (CI) Firebase มี Firebase Test SDK สำหรับ Cloud Functions เพื่อให้การทดสอบง่ายขึ้น SDK นี้เผยแพร่ใน 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 ภายในโฟลเดอร์ functions แล้วสร้างไฟล์ใหม่ภายในโฟลเดอร์นั้นสำหรับโค้ดทดสอบ และตั้งชื่อไฟล์ เช่น index.test.js

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

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

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

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

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

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

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

หากต้องการเขียนการทดสอบที่โต้ตอบกับโปรเจ็กต์ทดสอบ คุณต้องระบุค่าการกำหนดค่า 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');

เริ่มต้น 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() ในโค้ดฟังก์ชัน คุณต้องสร้างสตับก่อนนำเข้าฟังก์ชัน ในไฟล์ทดสอบ ให้กำหนดให้ 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 ใช้พารามิเตอร์ 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 เพื่อสร้าง DocumentSnapshot ของ Firestore อาร์กิวเมนต์แรกคือข้อมูล และอาร์กิวเมนต์ที่ 2 คือเส้นทางการอ้างอิงแบบเต็ม และมีอาร์กิวเมนต์ที่ 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

กำลังใช้ข้อมูล Stub (สำหรับโหมดออฟไลน์)

หากคุณเริ่มต้น SDK ในโหมดออฟไลน์ และกำลังทดสอบฟังก์ชัน Cloud Firestore หรือ Realtime Database คุณควรใช้ออบเจ็กต์ธรรมดาที่มีสตับแทนการสร้าง DocumentSnapshot หรือ DataSnapshot จริง

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

// 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)

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

  • Stub admin.database() เนื่องจาก addMessage() พุชไปยังฐานข้อมูลเรียลไทม์ ในโหมดออฟไลน์ คุณได้สร้างสตับ 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);

การล้างข้อมูลการทดสอบ

เรียกใช้ฟังก์ชันล้างข้อมูลที่ส่วนท้ายสุดของโค้ดทดสอบ การดำเนินการนี้จะยกเลิกการตั้งค่าตัวแปรสภาพแวดล้อมที่ SDK ตั้งค่าไว้เมื่อเริ่มต้น และลบแอป Firebase ที่อาจสร้างขึ้นหากคุณใช้ SDK เพื่อสร้างฐานข้อมูลแบบเรียลไทม์ DataSnapshot หรือ Firestore DocumentSnapshot

test.cleanup();

ดูตัวอย่างที่เสร็จสมบูรณ์และดูข้อมูลเพิ่มเติม

คุณดูตัวอย่างที่เสร็จสมบูรณ์ได้ในที่เก็บ GitHub ของ Firebase

ดูข้อมูลเพิ่มเติมได้ที่เอกสารอ้างอิง API สำหรับ firebase-functions-test