Viết unit test API với Sinon trong NodeJS


1044

Lời giới thiệu

Unit test rất mạnh mẽ và cần thiết cho việc phát triển dự án / sản phẩm NodeJS, nhưng làm thế nào để viết nó khi gọi các API của bên thứ ba từ bên ngoài luôn là một vấn đề nan giải đối với các lập trình viên. Ở đây tôi muốn trình bày cách sử dụng Sinon để mô phỏng các dịch vụ / API khác nhau trong unit test với stack NodeJS.

Workflow

Mục tiêu của chúng ta là controller lập lịch, controller này sẽ lập lịch offer cho các khách hàng khác nhau vào những thời điểm khác nhau trong app NodeJS. Cần có một tính năng mới để thêm chức năng tải tệp lên trong khi lên lịch ưu đãi. Quy trình làm việc là:

HTTP request->xác thực user->gửi đi message->schedulesController

Tuy nhiên, sau khi thêm chức năng tệp tải lên, nó phải tải tệp lên trong phiếu mua hàng này bằng cách thêm lệnh tải tệp mới lên hàng đợi công việc cũng như gọi một dịch vụ khác qua API để tải lên các tệp còn lại (tệp về loại ô tô), được lưu trữ trong một dịch vụ khác. Vì vậy, quy trình làm việc mới trở thành

HTTP request->xác thực user->gửi đi message->schedulesController-> upload files trong controller -> send request cho api bên thứ 3 để upload file

Vì vậy, để hoàn thành bài kiểm tra đơn vị này, chúng ta phải mô phỏng các chức năng của VerifyUser / DispatchMessage / addFileUploadJob / gọi API của bên thứ ba. Nghe có vẻ thú vị? hãy bắt đầu nó!

Setup

  • Tôi sử dụng Sinon để làm mock service, supertest để thực hiện yêu cầu API và chai.
  • Như đã phân tích trước đó, chúng ta cần biết những lib / object nào chúng ta cần mô phỏng
  • Sau đó, chúng ta có thể tạo sandbox từ Sinon để bắt đầu!
let sandbox = sinon.createSandbox();
  • Chúng ta cần import lib “node-fetch” vì phát lệnh gọi API từ app NodeJS được kích hoạt. Sau khi đọc source code, bởi fetch object từ node-fetch. Trong trường hợp này, để mô phỏng / khai thác lệnh request Http từ tìm nạp, chúng ta cần khai thác method Promise của object fetch, đó là mẹo nhỏ!
sandbox.stub(fetch, "Promise").returns(the reponse you want to get when calling your api);

// test framework set up, we use sinon to mock and chai for expect syntax, and request to call the app's request
const sinon = require("sinon");
const request = require("supertest");
const expect = require("chai").expect;
// objects need to be stubbed
const offerQuery = require("other libs");
const { uploadFileByIdQueue } = require("../../../other libs");
const libAuthRoles = require("other libs");
const fetch = require("node-fetch"); // for http request/api call mock, so we have to import node-fetch too, because in the source code, this api call is written with node-fetch
const libEvents = require("other libs too");
// some test data
const uuid = require("uuid");
const offerId = "offerId1";
const fileId1 = uuid();
const fileId2 = uuid();
const propertyId = "propertyId1";
const carTypeId = "carTypeId2";
const cookie = "fakeCookie";
// constant parameters for queue
const {
  DEFAULT_ATTEMPTS,
  DEFAULT_TIMEOUT
} = require("constant file");
// entry point of sinon, creating a sandbox
let sandbox = sinon.createSandbox();
let testApp = null;

Chuẩn bị

Đừng quên sandbox.restore () ở đầu tất cả code

Cú pháp chính để mock với Sinon nếu bạn cần trả về promise trong NodeJS

sandbox.stub(objectToMOck, methodToMock).returns(Promise.resolve(the values you want to for the test))
  • Một phương pháp hay là thêm error throw nếu bạn biết sau này bạn cần khởi tạo lại stub
sendUploadCarFileRequest = sandbox.stub(fetch, "Promise").throws(new Error("node-fetch: Not stubbed"));
beforeEach(() => {
  // restore all the stubs; remove all the existing stubs
  sandbox.restore();
  // to mock the function offerQuery, which is imported above from another file, this means,
  // during the flow of the test, any time encoutering this function, the behavior will be mocked rather than
  // real execution, since there is dependancy. so for offerQuery, mock returns a fake json format offer
  sandbox.stub(offerQuery, "get").returns(
    Promise.resolve(
      build("offer", {
        offerId: offerId,
        packages: [
          { propertyId: propertyId, carTypeId: carTypeId }
        ],
        Filetures: [
          { fileId: fileId1 },
          { fileId: fileId2 }
        ],
        schedules: [{ brand: "shouldNotUpload" }, { brand: "shouldUpload" }]
      })
    )
  );
  // if you need to recall the mock or re-build the stub during execution, you have to give it a alias/name;
  // also .throw(new Error) is helpful in terms of reminding this mock is initialized but not implememnted yet
  stubuploadFileByIdQueue = sandbox
    .stub(uploadFileByIdQueue, "add")
    .throws(new Error("uploadFileByIdQueue Add: Not stubbed"));
  // sandbox.stub(object, method).withArgs() helps when you want to return a specific value
  // while the input is also specific only while calling the stub
  stubuploadFileByIdQueue
    .withArgs(
      { fileId: fileId1 },
      {
        jobId: fileId1,
        attempts: DEFAULT_ATTEMPTS,
        timeout: DEFAULT_TIMEOUT
      }
    )
    .returns({ id: fileId1 });
  stubuploadFileByIdQueue
    .withArgs(
      { fileId: fileId2 },
      {
        jobId: fileId2,
        attempts: DEFAULT_ATTEMPTS,
        timeout: DEFAULT_TIMEOUT
      }
    )
    .returns({ id: fileId2 });
  // mock libAuthoRoles.verifyUser
  sandbox
    .stub(libAuthRoles, "verifyUser")
    .callsFake(() => (req, res, next) => {
      req.user = {
        roles: ["admin-user"]
      };
      next();
    });
  // mock libEvents.dispatch
  sandbox.stub(libEvents, "dispatch").resolves();
  // here is how we normally dealing with mock of another service/http api,
  // if this api call is triggered by fetch lib, you can use sandbox.stub(fetch,"Promise")
  // to mock this, so everytime while unit testing runs, when it hits the api, the app will
  // fake the return value as you set/expect.
  sendUploadCarFiletureRequest = sandbox
    .stub(fetch, "Promise")
    .throws(new Error("node-fetch: Not stubbed"));
  testApp = require(`../../../app`);
});

Testing

  • Vì có nhiều trường hợp thử nghiệm mà phải trả về các giá trị khác nhau (thành công hoặc không thành công). Chúng ta sẽ khởi tạo stub trong từng trường hợp thử nghiệm riêng biệt trong app NodeJS
sendUploadCarFileRequest.returns(Promise.resolve(responseObject));
  • Câu lệnh được sử dụng phổ biến nhất từ ​​Sinon là
sinon.assert.calledTwice(object.method);
sinon.assert.calledWith(parameters and their values)
sinon.assert.calledOnce(object.method);
sinon.assert.notCalled(object.method);

it("should add uploadFile jobs to the queue while scheduling to shouldUpload Partner", async function(done) {
  //mock for the response of calling api, so we know it's the api will return the carTypeId we send it to this api to show that the request is fully fulfilled
  jsonObject = { car_type_id: carTypeId };
  var responseObject = {
    status: "201",
    json: () => {
      return jsonObject;
    }
  };
  //sendUploadCarFiletureRequest is a mock of fetch, so it will return a promise with json response
  sendUploadCarFiletureRequest.returns(Promise.resolve(responseObject));
  // create the input for the controller call
  var end = new Date();
  const data = {
    brand: "shouldUpload",
    end: end.toISOString(),
    region: "CN",
    start: "2019-10-01T15:19:54+11:00",
    type: "availability"
  };
  // delete the pontentially duplicated records
  Schedule.destroy({
    where: {
      brand: data.brand,
      region: data.region,
      type: data.type,
      offerId: offerId
    }
  });
  // we use supertest to send the api request, you can also use other http libs you like
  request(testApp)
    .post(`/api/offers/${offerId}/schedules`)
    .set("Accept", "application/json")
    .set("Cookie", cookie)
    .send(data)
    .end((err, res) => {
      if (err) return done(err);
      expect(res.status).to.equal(201);
      // because we created two filetures to upload, so we expecte uploadFileByIDQueue.add should be used/called 2 times
      sinon.assert.calledTwice(uploadFileByIdQueue.add);
      // we expected the parameters to call these 2 jobs should be like the below
      sinon.assert.calledWith(
        uploadFileByIdQueue.add,
        { fileId: fileId1 },
        {
          jobId: fileId1,
          attempts: DEFAULT_ATTEMPTS,
          timeout: DEFAULT_TIMEOUT
        }
      );
      sinon.assert.calledWith(
        uploadFileByIdQueue.add,
        { fileId: fileId2 },
        {
          jobId: fileId2,
          attempts: DEFAULT_ATTEMPTS,
          timeout: DEFAULT_TIMEOUT
        }
      );
      //we verify that the api for remote service is called too
      sinon.assert.calledOnce(sendUploadCarFiletureRequest);
      done();
    });
});

Kết luận

Sinon là một thư viện rất hữu ích và linh hoạt để triển khai. Có thể mất một lúc để làm quen và thiết lập. Tuy nhiên, tôi cảm thấy tự tin hơn rất nhiều sau khi đã pass các unit test. Và trên thực tế, việc thiết lập chỉ là việc một sớm một chiều, sau khi bạn biết cách thực hiện, tôi chắc chắn bạn sẽ được hưởng lợi ích ngay lập tức.

Tham khảo thêm về NodeJS: Tạo bot Telegram đơn giản bằng NodeJS

Tham khảo thêm về React: Các xây dựng Micro Frontend với React và SSR


Like it? Share with your friends!

1044