Kiểm thử đơn vị chức năng đám mây

Trang này mô tả các phương pháp hay nhất và công cụ để viết mã kiểm thử đơn vị cho các hàm của bạn, chẳng hạn như các kiểm thử thuộc hệ thống Tích hợp liên tục (CI). Để giúp thử nghiệm dễ dàng hơn, Firebase cung cấp SDK thử nghiệm của Firebase cho chức năng đám mây. Phiên bản này được phân phối trên npm dưới dạng firebase-functions-test và là một SDK thử nghiệm đồng hành cho firebase-functions. SDK kiểm thử của Firebase cho chức năng đám mây:

  • Chú ý đến việc thiết lập và chia nhỏ thích hợp cho các chương trình kiểm thử, chẳng hạn như đặt và huỷ thiết lập các biến môi trường cần thiết cho firebase-functions.
  • Tạo dữ liệu mẫu và ngữ cảnh sự kiện để bạn chỉ phải chỉ định các trường liên quan đến thử nghiệm của mình.

Thiết lập thử nghiệm

Cài đặt cả firebase-functions-testMocha (một khung kiểm thử) bằng cách chạy các lệnh sau trong thư mục hàm:

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

Tiếp theo, hãy tạo một thư mục test bên trong thư mục hàm, tạo một tệp mới bên trong thư mục đó cho mã kiểm thử và đặt tên như index.test.js.

Cuối cùng, hãy sửa đổi functions/package.json để thêm nội dung sau:

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

Sau khi viết chương trình kiểm thử, bạn có thể chạy chương trình kiểm thử đó bằng cách chạy npm test bên trong thư mục hàm.

Khởi chạy Firebase Test SDK cho Cloud Functions

Có hai cách để sử dụng firebase-functions-test:

  1. Chế độ trực tuyến (nên dùng): Viết mã kiểm thử tương tác với một dự án Firebase dành riêng cho việc kiểm thử để việc ghi cơ sở dữ liệu, người dùng tạo, v.v. sẽ thực sự diễn ra, đồng thời mã kiểm thử của bạn có thể kiểm tra kết quả. Điều này cũng có nghĩa là các SDK khác của Google được sử dụng trong chức năng của bạn cũng sẽ hoạt động.
  2. Chế độ ngoại tuyến: Viết các bài kiểm thử đơn vị riêng lẻ và ngoại tuyến mà không có tác dụng phụ. Điều này có nghĩa là mọi lệnh gọi phương thức tương tác với một sản phẩm Firebase (ví dụ: ghi vào cơ sở dữ liệu hoặc tạo người dùng) đều cần phải được loại bỏ. Nhìn chung, bạn không nên sử dụng chế độ ngoại tuyến nếu có hàm Cloud Firestore hoặc cơ sở dữ liệu theo thời gian thực, vì chế độ này làm tăng đáng kể độ phức tạp của mã kiểm thử.

Khởi chạy SDK ở chế độ trực tuyến (nên dùng)

Nếu muốn viết các chương trình kiểm thử tương tác với một dự án kiểm thử, bạn cần cung cấp các giá trị cấu hình dự án cần thiết để khởi chạy ứng dụng thông qua firebase-admin và đường dẫn đến tệp khoá tài khoản dịch vụ.

Cách lấy giá trị cấu hình của dự án Firebase:

  1. Mở phần cài đặt dự án trong bảng điều khiển của Firebase.
  2. Trong phần Ứng dụng của bạn,hãy chọn ứng dụng bạn muốn.
  3. Trong ngăn bên phải, hãy chọn tuỳ chọn để tải tệp cấu hình xuống cho các ứng dụng của Apple và Android.

    Đối với ứng dụng web, hãy chọn Config (Cấu hình) để hiển thị các giá trị cấu hình.

Cách tạo tệp khoá:

  1. Mở ngăn Tài khoản dịch vụ của bảng điều khiển Google Cloud.
  2. Chọn tài khoản dịch vụ mặc định của App Engine và sử dụng trình đơn tuỳ chọn ở bên phải để chọn Create key (Tạo khoá).
  3. Khi được nhắc, hãy chọn JSON cho loại khoá rồi nhấp vào Tạo.

Sau khi lưu tệp khoá, hãy khởi chạy 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');

Khởi chạy SDK ở chế độ ngoại tuyến

Nếu muốn viết kiểm thử hoàn toàn ngoại tuyến, bạn có thể khởi chạy SDK mà không cần bất kỳ tham số nào:

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

Mô phỏng giá trị cấu hình

Nếu sử dụng functions.config() trong mã hàm, bạn có thể mô phỏng các giá trị cấu hình. Ví dụ: nếu functions/index.js chứa mã sau:

const functions = require('firebase-functions');
const key = functions.config().stripe.key;

Sau đó, bạn có thể mô phỏng giá trị bên trong tệp kiểm thử như sau:

// Mock functions config values
test.mockConfig({ stripe: { key: '23wr42ewr34' }});

Nhập hàm của bạn

Để nhập các hàm, hãy sử dụng require để nhập tệp hàm chính dưới dạng một mô-đun. Hãy nhớ chỉ thực hiện việc này sau khi khởi chạy firebase-functions-test và mô phỏng các giá trị cấu hình.

// after firebase-functions-test has been initialized
const myFunctions = require('../index.js'); // relative path to functions code

Nếu đã khởi tạo firebase-functions-testchế độ ngoại tuyến và có admin.initializeApp() trong mã hàm, thì bạn cần phải đặt mã nguồn trước khi nhập hàm:

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

Kiểm thử các hàm nền (không phải HTTP)

Quy trình kiểm thử các hàm không phải HTTP bao gồm các bước sau:

  1. Gói hàm mà bạn muốn kiểm thử bằng phương thức test.wrap
  2. Xây dựng dữ liệu kiểm thử
  3. Gọi hàm được bao bọc bằng dữ liệu kiểm thử mà bạn đã tạo và bất kỳ trường ngữ cảnh sự kiện nào mà bạn muốn chỉ định.
  4. Đưa ra khẳng định về hành vi.

Trước tiên, hãy gói hàm bạn muốn kiểm thử. Giả sử bạn có một hàm trong functions/index.js tên là makeUppercase mà bạn muốn kiểm thử. Viết nội dung sau trong functions/test/index.test.js

// "Wrap" the makeUpperCase function from index.js
const myFunctions = require('../index.js');
const wrapped = test.wrap(myFunctions.makeUppercase);

wrapped là một hàm gọi makeUppercase khi được gọi. wrapped lấy 2 tham số:

  1. data (bắt buộc): dữ liệu cần gửi đến makeUppercase. Tham số này trực tiếp tương ứng với tham số đầu tiên được gửi đến trình xử lý hàm mà bạn đã viết. firebase-functions-test cung cấp các phương thức để tạo dữ liệu tuỳ chỉnh hoặc dữ liệu mẫu.
  2. eventContextOptions (không bắt buộc): các trường của ngữ cảnh sự kiện mà bạn muốn chỉ định. Ngữ cảnh sự kiện là thông số thứ hai được gửi đến trình xử lý hàm mà bạn đã viết. Nếu bạn không đưa tham số eventContextOptions vào khi gọi wrapped, thì ngữ cảnh sự kiện vẫn được tạo bằng các trường hợp lý. Bạn có thể ghi đè một số trường đã tạo bằng cách chỉ định các trường đó tại đây. Lưu ý rằng bạn chỉ phải bao gồm các trường mà bạn muốn ghi đè. Mọi trường mà bạn không ghi đè sẽ được tạo.
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
});

Xây dựng dữ liệu kiểm thử

Tham số đầu tiên của hàm được bao bọc là dữ liệu kiểm thử để gọi hàm cơ bản. Có một số cách để xây dựng dữ liệu kiểm thử.

Sử dụng dữ liệu tuỳ chỉnh

firebase-functions-test có một số hàm để tạo dữ liệu cần thiết để kiểm thử các hàm của bạn. Ví dụ: sử dụng test.firestore.makeDocumentSnapshot để tạo DocumentSnapshot trên Firestore. Đối số đầu tiên là dữ liệu và đối số thứ hai là đường dẫn tham chiếu đầy đủ. Ngoài ra còn có một đối số thứ ba không bắt buộc cho các thuộc tính khác của bản tổng quan nhanh mà bạn có thể chỉ định.

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

Nếu đang kiểm thử một hàm onUpdate hoặc onWrite, bạn cần tạo 2 ảnh chụp nhanh: một cho trạng thái trước và một cho trạng thái sau. Sau đó, bạn có thể sử dụng phương thức makeChange để tạo đối tượng Change bằng các bản tổng quan nhanh này.

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

Hãy xem tài liệu tham khảo API để biết các hàm tương tự cho tất cả kiểu dữ liệu khác.

Sử dụng dữ liệu mẫu

Nếu bạn không cần tuỳ chỉnh dữ liệu dùng trong chương trình kiểm thử, thì firebase-functions-test sẽ cung cấp các phương thức để tạo dữ liệu mẫu cho từng loại hàm.

// For Firestore onCreate or onDelete functions
const snap = test.firestore.exampleDocumentSnapshot();
// For Firestore onUpdate or onWrite functions
const change = test.firestore.exampleDocumentSnapshotChange();

Hãy xem tài liệu tham khảo API để biết các phương thức lấy dữ liệu mẫu cho mọi loại hàm.

Sử dụng dữ liệu được xếp sẵn (cho chế độ ngoại tuyến)

Nếu đã khởi tạo SDK ở chế độ ngoại tuyến và đang kiểm thử chức năng Cloud Firestore hoặc Cơ sở dữ liệu theo thời gian thực, bạn nên sử dụng đối tượng thuần tuý có các mã giả lập thay vì tạo DocumentSnapshot hoặc DataSnapshot thực tế.

Giả sử bạn đang viết một chương trình kiểm thử đơn vị cho hàm sau:

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

Bên trong hàm này, snap được sử dụng hai lần:

  • snap.val()
  • snap.ref.parent.child('uppercase').set(uppercase)

Trong mã kiểm thử, hãy tạo một đối tượng thuần tuý nơi cả hai đường dẫn mã này sẽ hoạt động và sử dụng Sinon để giả lập các phương thức.

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

Đưa ra khẳng định

Sau khi khởi chạy SDK, gói hàm và tạo dữ liệu, bạn có thể gọi các hàm được gói bằng dữ liệu đã tạo và đưa ra nhận định về hành vi. Bạn có thể sử dụng một thư viện như Chai để đưa ra những câu nhận định này.

Đưa ra nhận định ở chế độ trực tuyến

Nếu đã khởi động SDK kiểm thử Firebase cho chức năng đám mây ở chế độ trực tuyến, bạn có thể xác nhận rằng các hành động mong muốn (chẳng hạn như ghi cơ sở dữ liệu) đã diễn ra bằng cách sử dụng SDK firebase-admin.

Ví dụ bên dưới xác nhận rằng 'INPUT' đã được ghi vào cơ sở dữ liệu của dự án kiểm thử.

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

Đưa ra câu nhận định ở chế độ ngoại tuyến

Bạn có thể nhận định về giá trị trả về dự kiến của hàm:

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

Bạn cũng có thể sử dụng gián điệp Sinon để xác nhận rằng một số phương thức nhất định đã được gọi và với các tham số bạn mong đợi.

Kiểm thử hàm HTTP

Để kiểm thử các hàm HTTP onCall, hãy sử dụng cùng một phương pháp như kiểm thử hàm trong nền.

Nếu đang kiểm thử các hàm HTTP onRequest, bạn nên sử dụng firebase-functions-test nếu:

  • Bạn sử dụng functions.config()
  • Hàm của bạn tương tác với một dự án Firebase hoặc các API khác của Google và bạn muốn sử dụng một dự án Firebase thực cũng như thông tin xác thực của dự án đó cho các thử nghiệm của mình.

Hàm onRequest HTTP có 2 tham số: đối tượng yêu cầu và đối tượng phản hồi. Dưới đây là cách bạn có thể kiểm thử hàm mẫu addMessage():

  • Ghi đè hàm chuyển hướng trong đối tượng phản hồi vì sendMessage() gọi hàm đó.
  • Trong hàm chuyển hướng, hãy dùng chai.assert để giúp đưa ra câu nhận định về những tham số mà hàm chuyển hướng sẽ được gọi:
// 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);

Kiểm thử thao tác dọn dẹp

Ở cuối mã kiểm thử, hãy gọi hàm dọn dẹp. Thao tác này sẽ huỷ đặt các biến môi trường mà SDK đã đặt khi khởi chạy, đồng thời xoá các ứng dụng Firebase có thể đã được tạo nếu bạn sử dụng SDK để tạo cơ sở dữ liệu theo thời gian thực DataSnapshot hoặc Firestore DocumentSnapshot.

test.cleanup();

Xem xét ví dụ hoàn chỉnh và tìm hiểu thêm

Bạn có thể xem lại các ví dụ hoàn chỉnh trên kho lưu trữ GitHub của Firebase.

Để tìm hiểu thêm, hãy tham khảo tài liệu tham khảo API cho firebase-functions-test.