API 自動化的技術選擇與 Modules 的分層思考

今年是 2022 年,我加入了一間提供 API 服務為主的公司,有很多基礎建設都還沒有存在,這也意味著有很多東西是需要溝通與建構的,其中就包含了將 API 測試案例自動化的基本建置,於是就產生了這篇紀錄,如果不想看心路歷程只想看 code 的話可以直接到 Github 上參考。

https://github.com/SQZ777/jest-for-api-automation-template.git

各個 subtitle 如下

  • 為何不使用 Postman 來做 API Automation
  • 語言選擇
  • 測試框架選擇
  • Modules 的分層
  • 各個 Modules 的介紹與實作
  • 結語和本篇沒提到的事

首先在使用程式碼撰寫 API 自動化的測試案例時,一定會先遇到這個問題

為何不使用 Postman 來做 API Automation?

所以就先來列一下 Postman 的優/缺點吧

優點如下

  • UI 介面容易使用,門檻極低
  • 可以透過 JavaScript 來實作檢查 response 的 script
  • 各個平台(Mac, Windows)都可以使用
  • 可以透過 newman(CLI)來建置 CI 的流程

接著列一下使用 Postman 執行管理自動化案例會遇到的問題

  • 大量自動化案例會產生極大的維護成本
    • 無法重用的 test script
    • 受測 API 如果新增 required fields,會需要手動更新大量既有的 script
  • 執行 collection runner 再透過 csv 檔案來匯入測試資料的過程過於繁瑣

Postman 在實作小型的 API 驗證時,是一個非常好用的工具,但需要實作大量 API 測試案例時,在 Postman 上管理這些案例會產生很大的維護成本,不信的話你可以試試,所以我們需要透過撰寫程式的方式來管理這些即將被實作的 API 測試案例。

語言選擇

首先要選擇的是語言,依照公司內部現有的技術而選,策略是以不增加公司同事之間跨越職能障礙為主要目標,其次是這個語言的資源,再其次是學習的門檻,因為有尚未開發過自動化的 QA ,所以選擇語言起手的難易度也需要列為考量。

抱歉了 Java XD

公司內部的 backend 是 Java,而 front-end 則是 Vue.js,所以就剩下 JavaScript 及 Java 的選項能夠選擇,考量到學習的難易度與學習資源取得的容易度,就選了 JavaScript 了,其中考量到 JavaScript 的原因還有就是公司是有需要驗證 Web 上面顯示資料正確性的需求,所以如果學會了 JavaScript ,就可以 JavaScript 實作一些工具直接在 console 上執行來協助測試。

測試框架選擇

接下來就可以找 JavaScript 在 2021 的統計,可以參考這個網站,可以從圖表看到使用率最高的是 Jest。

使用率最高不代表他就是一個值得讓人使用的框架,所以再次參考了「時間推移體驗」這個數據,也可以觀察到 Jest 是一個會讓人願意再次使用的框架。

所以依照上面的數據,問了幾個身邊的朋友,得到的回饋也是好的,所以就選擇了 Jest 作為這次實作自動化的框架。

Modules 的分層

為了解決 Postman 所遇到的痛點,所以我們需要

  • tests 層
    • 使用其他 modules 來組成 test case 的地方
  • request API 的部分要抽成 apis 的 module
  • request payloads 的部分要抽成 requestPayloads 的 module
  • 有一些需要共用的 lib 抽成 common 的 module

相依的關係可以畫成這個樣子

資料夾結構如下

├─apis
│      apis1.js
│      apis2.js
│      api{...}.js
│      index.js
│      requestHelper.js
├─common
│      jestExtend.js
│          
├─requestPayloads
│      apis1Request.js
│      apis2Request.js
│      apis{...}Request.js
│      index.js
│      
└─tests
    │   singleApi1.test.js
    │   singleApi2.test.js
    │   singleApi{...}.test.js
    │      
    └─stories
           stories.test.js

各個 Modules 的介紹與實作

requestPayloads 資料夾

requestPayloads 資料夾中的 indejx.js 是用來統整各個 API 的預設 request payloads

const apis1= require('./apis1Request');
const apis2= require('./apis2Request');

module.exports = {
  apis1,
  apis2,
};

預設 request payloads 的定義為:Server 不會回應「lack of fields response」的 payload

以路徑 /apis1/products 為例子,會取 apis1 這個詞當作 file name,然後 products 當作 function name,apis1Request.js 內容就會如下:

const products = {
  product_id: 1,
  product_info: {
    product_name: 'Car Engine',
  },
};

module.exports = {
  products,
};

假設 api 路徑為 /apis1/products,預期在 tests 中使用時則是這個樣子(第 4 行)

const apis = require('../apis');
const requestPayloads = require('../requestPayloads');

const apis1ProductsRequest = requestPayloads.apis1.products;
const result = await apis.apis1.products(apis1ProductsRequest );
expect(result.productName).toBe('something that expected product name');

apis 資料夾

apis 資料夾中的 index.js 是用來統整 apis1, apis2…等 api 的地方

const apis1 = require('./apis1');
const apis2 = require('./apis2');

module.exports = {
  apis1,
  apis2,
};

apis 資料夾中的 requestHelper.js 是用來管理 request API 的 HTTP method 的一層,如 get, post 等 在這一層會與 report 那一層作結合,多讓 jest 的 report 多帶一些在打 api request 的相關結果 程式碼單純以 post 為例子

const axios = require('axios');

/**
 * @param {string} baseURL for base URL
 * @param {object} headers for request headers
 * @param {object} data for request payload
 */
async function postRequest(baseURL, headers, data) {
  const result = await axios({
    method: 'post',
    url: baseURL,
    headers: headers,
    data: data,
  })
      .then((result) => {
        return result;
      })
      .catch((err) => {
        console.log(err);
      });
  return result;
}

module.exports = {
  postRequest,
};

以路徑 /apis1/products 為例子,會取 apis1 這個詞當作 file name,然後 products 當作 function name,apis.js 中則會引用到 requestHelper.js 來 request API,apis.js code 如下

const {postRequest} = require('./requestHelper');
require('dotenv').config();
/**
 * @param {object} request payload
 * @return {object} response
 */
**async function products(data) {
  const result = await postRequest(
      `${configs.BASE_URL}/apis1/products`,
      {
        'x-api-key': process.env.API_KEY,
        'content-type': 'application/json',
      },
      data,
  );
  return result;
}**

假設 api 路徑為 /apis1/products,預期在 tests 中使用時則是這個樣子(第 5 行)

const apis = require('../apis');
const requestPayloads = require('../requestPayloads');

const apis1ProductsRequest = requestPayloads.apis1.products;
const result = await apis.apis1.products(apis1ProductsRequest );
expect(result.productName).toBe('something that expected product name');

common 資料夾

這一層主要是放一些官方沒有提供的 library 實作,或是共用的 function,以 jest 沒有提供的 object contain 為例,就會新增 jestExtend.js,其 code 如下,在 scenario API test 中將會用到。

expect.extend({
  toContainObject(received, argument) {
    const pass = this.equals(
        received,
        expect.arrayContaining([expect.objectContaining(argument)]),
    );

    if (pass) {
      return {
        message: () =>
          `expected ${this.utils.printReceived(
              received,
          )} not to contain object ${this.utils.printExpected(argument)}`,
        pass: true,
      };
    } else {
      return {
        message: () =>
          `expected ${this.utils.printReceived(
              received,
          )} to contain object ${this.utils.printExpected(argument)}`,
        pass: false,
      };
    }
  },
});

tests 資料夾

這一層就是 Jest 的使用層了,會在這一層中使用各個 modules 來組成 test case。

API 測試種類大致上可以分成兩種

  • 單一 API 測試 (Single API test)
  • API 情境測試 (Scenario API test)

單一 API 測試是指純粹只有這隻 API 是受測項目,完成測試的條件與其他 API 無關,是一個只需要使用單一一個 API 的測試項目,舉例來說 /products API,帶給他 payload,其中會 lack fields,或是沒有帶 header,就稱之為 single API test。

API 情境測試是指要完成一個測試情境而需要用到多個 API,這裡就會需要引用到多個不同的 API 來達成某種目的,舉例來說 call /prodcuts/update 更新一個 product 然後再透過 /products API 來取得預期被更新的 API,這邊就會引用到兩隻 API,這時候就稱之為 API 的情境測試。

Single API test,以 /products API 為例子,其 code 如下

const apis = require('../apis');

test('Get products, should return 200', async () => {
  const result = await apis.products.get();
  expect(result.status).toBe(200);
});

Scenario API test,以 /products, /products/create 為例子,其 code 如下

require('../../common/jestExtend');
const apis = require('../../apis');
const requestPayloads = require('../../requestPayloads');

test('Create product, should get the product at /products', async () => {
  const createResult = await apis.products.create(
      requestPayloads.products.create,
  );
  expect(createResult.status).toBe(200);

  const result = await apis.products.get();

  expect(result.data).toContainObject(requestPayloads.products.create);
});

結語和本篇沒提到的事

在這一篇中沒提到的有以下幾個事情

  • json schema 的 validate
  • config

依照目前的架構要擴充這兩件事情都可以很輕鬆,所以就沒有額外再寫出來記錄了

以上就是這一次整個 API automation 的思考與選擇的筆記
感謝各位大大看到這裡,如果有任何建議都可以跟我說,感謝 <(_ _)>

再次附上 repo: SQZ777/jest-for-api-automation-template (github.com)