AC 關鍵字計畫 | API 開發、測試、除錯一次到位!使用 MSW 快速上手 Mock API

AC 關鍵字計畫 | API 開發、測試、除錯一次到位!使用 MSW 快速上手 Mock API

本篇文章已刊載在AC Blog

什麼是 API

在認識 API 以前,我們直接從生活中的例子來認識介面 (Interface)。

忙碌的午後,「小美」想要喝罐可爾必思。於是她前往公司樓下的飲料販賣機,在「操作面板上」投硬幣跟按按鈕,販賣機裡的「系統」判斷金額是否符合,以及是按下哪個飲料的對應按鈕。判斷結束後,就把一罐可爾必思推出來。最後小美從取物口拿出,大口地暢飲。

在上述例子裡,飲料販賣機的控制面板即為人機互動的「介面」,也就是等下待會要講的「API」。而小美呢,則是介面的使用者,對應到軟體開發的領域上是指 API 的使用者,也就是我們這些應用程式的開發者。

API(Application Programming Interface)的字面意思是「應用程式介面」。就像飲料販賣機的介面一樣,API 是應用程式之間一套明確定義的溝通方法和接口。使用 API 的過程中,不需要理解內部複雜的運作,只需要知道要呼叫什麼方法,傳入對應參數,就能取得想要的結果。

Web 應用程式中的 API

在 Web 應用程式的開發情境下的 API 被稱為 Web API,在 Web API 作用時,客戶端(前端)和伺服器端(後端)會透過 HTTP 通訊協定來進行請求與回應。

原圖取自:AlphaCamp Blog

作為前端的開發者,為了要取得存放在系統的資訊,常常會需要以 Web API 來發送請求與後端溝通。因此通常需要做的事情有以下:

  • 閱讀 API 文件
  • 如果是第三方開發的應用(例如 Google、Facebook),可能需要註冊、驗證等程序
  • 撰寫有關 Web API 串接的函式
  • 在適當時機點呼叫對應的串接函式

而後端的開發者,則是將擁有的資料庫開放給第三方使用。因此開發步驟大致會是這樣:

  • 撰寫一個明確的 API 文件,清楚定義 API 的使用方式及格式等等。
  • 按照文件開發一個 Web API,提供可外部呼叫的方法,用這個方式做為使用介面。

控制 API 的回傳結果,可能嗎?

前端開發時,十之八九都會需要實作 API 的串接。你可能會先建立相關的函式模組,然後在元件或是狀態管理的程式碼中匯入,在適當時機點發出 API 的請求。

問題來了,如果想要測試 API 回傳不同的結果下,元件的操作邏輯是否正常;或是現在正處於雛型(Prototype)開發的階段,在後端的 Web API 還沒完成的情況下,前端要超前部署產出具體的半成品等,前端這邊是不是可以先實作好 API 串接的部分,而且還能隨意控制 API 回傳的結果呢?

舉個簡單的例子,在 api.js 中,定義了所有跟 API 串接的函式模組,其中,isLoggedIn 函式是檢查使用者是否已登入。

  • api.js
1
2
3
4
5
6
7
export async function isLoggedIn() {
axios.post("/api/account/isLoggedIn")
.then(response => {
// ...
});
}
// ...

在元件- App.js 中,我們匯入isLoggedIn 函式,並且在適當時機點呼叫它,如果已經登入,執行 A,否則執行 B。

  • App.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { isLoggedIn } from './api';

function App() {
//...
await function initApp() {
const isLoggedIn = await isLoggedIn();
if(isLoggedIn) {
// 做 A 事...
} else {
// 做 B 事...
}

}
//...
React.useEffect(() => {
initApp();
}, []);
//...
}

假設不變動 App.js 中的-

1
const isLoggedIn = await isLoggedIn();

我們有辦法由前端控制 isLoggedIn 變數是 true 還是 false,來檢查相關的條件判斷嗎?

這個問題,當然是 YES 囉!這就是今天要講的-Mock API

API 開發、測試、除錯一次到位-Mock API

先來說文解字一下,「Mock」是從單元測試的概念延伸而來的,主要是模擬真實的物件或類別,但是省略內部複雜的行為,讓測試過程可以更專注在本身的測試重點上。

原圖取自:程式札記

同樣地,比起透過真實的後端 server 取得資料,我們有時候會比較想要自己決定資料的樣子,甚至是定義 API 的入口點(entrypoint)和參數欄位等等,但同時又不想變動前端呼叫 API 串接的部分。可以完成以上種種行為的方式,就是 Mock API。

使用 Mock API 的理由

最普遍的使用需求,應該就是希望在初期的開發階段,前端不需要等後端實作完成後才能接著進行。除了讓開發時程不用花到雙倍的時間,前後端雙方也能透過 Mock API 的過程更具體討論實作細節,達成品質與速度都能雙贏的目的 💪

除此之外,以下的場合也能應用 Mock API 來完成:

  • 撰寫測試案例:進行元件測試或 E2E 測試時,可以撰寫各種 HTTP 狀態,或是不同回應資料時的測試案例。
  • 開發時的除錯:藉由修改 mocking 的內容,幫助元件或頁面進行除錯。

有沒有 mock API 的時程與品質比較

向 MSW Say Hi!

原圖取自:Mock Service Worker

MSW(Mock Service Worker)是一套可以實現 Mock API 的工具。最大的特色是能在網路層(Network)發出實際的請求(Request),並透過 Service Worker 攔截,回傳已經定義好的資料內容。

另外,MSW 也擁有許多優點,像是:

  • 跟許多框架、工具都有很好的整合性,像是 React、Jest、Cypress 等。
  • 針對主流的 API 設計-RESTful 和 GraphQL 都有良好的支援。
  • 如果專案是以 TypeScript 開發,不須特別設定就能直接使用 TypeScript,享受強型別帶來的便利與好處。
  • 清楚好閱讀的官方文件,並且有範例程式,讓開發者能好上手。

因此跟其他相似的工具來比較,像是 MirageJS、json-server,MSW 的下載使用量平均超過它們的兩倍,並且有成長的趨勢。這也表示相關的社群資源和討論聲量也會愈來愈豐富。

原圖取自:npm trends

在熟悉 MSW 之前,我們先基礎認識它的底層機制-Service Worker,以及知道 RESTful 和 GraphQL 的差別。

Service Worker

Service Worker 是 Web Worker 的一種。除了可以在背景執行緒中運作,不影響使用者的網頁瀏覽體驗以外,最大特色是透過 cache 的機制來打造離線體驗。我們多少會碰到一些網站提供推播通知、暫存資料或是背景中同步等功能,這要多虧有 Service Worker 的實現。

原圖取自:netlify

導入 Service Worker 有幾個重點步驟:

  1. 檢查瀏覽器的支援度:基本上除了IE,大部分瀏覽器在某版本之後都有陸續支援,如果需要,可以加上檢查。
1
2
3
if ('serviceWorker' in navigator) {
// ...
}
  1. 註冊:呼叫全域方法 navigator.serviceWorker.register() 進行註冊。由於這是個 promise 函式,可以透過 thencatch 來做完成後的處理。
1
2
3
navigator.serviceWorker.register('/serviceWorker.js')
.then(register => {})
.catch(error => {});
  1. 安裝:註冊完後,會觸發 serviceWorker.js 中的 install 事件,執行瀏覽器的離線快取功能。
1
2
3
this.addEventListener('install', function () {
// ...
})
  1. 啟動:註冊完後,會觸發 serviceWorker.js 中的 active 事件,正式啟動 Service Worker。
1
2
3
this.addEventListener('active', function () {
// ...
})
  1. 存取請求內容:當在這個 Service Worker 的可控範圍內偵測到 HTTP 請求,會觸發 serviceWorker.js 中的 fetch 事件實作 cache 的機制。
1
2
3
this.addEventListener('fetch', async function (event) {
// ...
}
  1. 收發訊息:觸發 serviceWorker.js 中的 message 事件,處理各種請求階段的接收與發送訊息。
1
2
3
this.addEventListener('message', function(e) {
// ...
});

RESTful vs. GraphQL

RESTful 是一種 API 的設計風格,將有語意性的詞彙加上資料參數形成特定路由(Route),並運用 HTTP 各種定義的請求方式,向後端取得資源或是修改資源。

因此 RESTful 最大的特點是,可以透過路由的結構清楚知道資料欄位跟操作方式,適合用來設計簡單或是結構較為單純的 API。

原圖取自:how to graphql

GraphQL 是由 Facebook 在 2015 年發布的 open source。透過前端定義的資料結構,告訴後端返回相同資料結構的對應資料,來避免後端設計大量的 API route ,或是返回多餘的資料欄位。

不過這種靈活性也增加了複雜性,通常會比較適合用來設計具規模或是結構比較交錯複雜的 API。

原圖取自:how to graphql

MSW 初體驗

如果只是想先體驗看看,不用大費周章建立環境,可以參考 MSW 建立的範例程式,裡面有許多不同的範本專案。切到不同的專案環境後,就可以執行相關指令玩玩看囉😊

  1. clone 範本專案。執行以下指令,或是使用 GitHub Desktop 的 Clone Repository > URL 輸入以下的網址部分。
1
git clone https://github.com/mswjs/examples.git
  1. 切換到 examples 目錄。
1
cd examples
  1. 安裝所有依賴套件。
1
2
yarn
# or npm install
  1. 切換到特定目錄,例如:想看使用 React,加上 RESTful 的 API 設計會是什麼樣子,那麼就切換到 rest-react
1
cd examples/rest-react
  1. 執行以下指令,在本機建立 live server。
1
yarn start
  1. 打開瀏覽器輸入 localhost:3001。正常的話,可以看到頁面上有個簡易表單。按 F12 觀察 console,就會出現 [MSW] Mocking enabled.的訊息,表示 MSW 已經在運作囉!

範例專案的起始頁面

  1. 可以把 Dev Tool 切到 Network 觀察。在表單欄位隨意輸入後送出,會發現 Network 中發出了一個 login 的請求。

Network中新增了一個login請求

  1. 再切回 Console,可以看到 MSW 有印出請求的 Request、Reponse 的詳細內容。找到程式碼來對照看看,的確 MSW 發揮了作用,以程式中定義好的資料作為 Response 的資料,magic~

Console中印出了login請求的詳細內容

建置與導入 MSW

如果想引入專案環境中,其實非常簡單,基本上安裝 msw 這個套件就可以了。

1
2
yarn add -DW msw
# or npm i msw -D

不過不少人,包括我自己是用 webpack 來打包前端程式和建立 live server。因此接下來的建置流程會再多介紹 webpack 的導入方式。

  1. 安裝 MSW 以及執行 webpack 時所需的 plugins,作為開發用的依賴套件。
1
2
yarn add -DW msw copy-webpack-plugin workbox-webpack-plugin
# or npm i msw copy-webpack-plugin workbox-webpack-plugin -D
  1. 在專案的根目錄下建立 public 的子目錄,如果已經有的話省略。

  2. src 的目錄下建立 mocks 的子目錄。

  3. mocks 的目錄下建立兩個檔案- handlers.ts(js) 以及 browser.ts(js),待會會提到更多這兩個檔案的實作部分。

  4. 在專案的根目錄下開啟終端機,執行以下指令來建立負責產生 Service Worker 的程式檔 mockServiceWorker.js。如果有興趣的話,可以從這份檔案來看 MSW 如何設計 Service Worker 的架構與各種生命週期的事件喔!

1
npx msw init public/
  1. webpack.config.js 或特定的 webpack 設定檔中,在 plugins 部分新增跟 Service Worker 相關的設定。
    • CopyWebpackPlugin- 將 mockServiceWorker.js 複製到 live server 中。
    • WorkboxPlugin- 建立 Service Worker。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const CopyWebpackPlugin = require('copy-webpack-plugin');
const WorkboxPlugin = require('workbox-webpack-plugin');

module.exports = {
// ...

plugins: [
//...
new CopyWebpackPlugin([
{ from: path.resolve(__dirname, './public', 'mockServiceWorker.js'), to: 'mockServiceWorker.js' },
]),
new WorkboxPlugin.GenerateSW(),
]
}
  1. index.html 偵測當前瀏覽器有無支援 Service Worker,有的話就以 mockServiceWorker.js 來註冊 Service Worker。
1
2
3
4
5
6
7
8
9
<script>
// Check that service workers are supported
if ('serviceWorker' in navigator) {
// Use the window load event to keep the page load performant
window.addEventListener('load', () => {
navigator.serviceWorker.register('/mockServiceWorker.js');
});
}
</script>

如果按照以上步驟做完的話,目錄結構應該會長這樣-

1
2
3
4
5
6
7
8
9
-- MyProject/
-- public/
-- mockServiceWorker.js
-- src/
-- mocks/
-- browser.ts(js)
-- handlers.ts(js)
-- index.html
-- webpack.config.js

快速上手 MSW

handlers.ts(js) 是負責撰寫 mock API 的實作細節。通常會建立一個陣列變數-handlers 來存放各個 API 串接的實作。

1
export const handlers = [];

舉上面範例程式的 login 請求作為例子,分別看看 RESTful 跟 GraphQL 這兩種 API 設計模式,MSW 會如何實作。

RESTful

從 msw 匯入 restrest 提供了各種模擬 HTTP 的請求方式實作-

  • rest.get():對應 GET 請求,取得資源。
  • rest.post():對應 POST 請求,提交指定資源的實體。
  • rest.put():對應 PUT 請求,上傳或取代指定的資源。
  • rest.patch():對應 PATCH 請求,指定資源的部份修改。
  • rest.delete():對應 DELETE 請求,刪除指定資源。
  • rest.options():對應 OPTIONS 請求,查詢 server 支援的 HTTP 請求方式。

這些方法需要固定傳入兩個參數-

  • entrypoint-API的路徑,可以 /:someData 的方式以路徑傳入資料參數。
  • resolver function-請求完成後的 resolve 函式。有三個參數可以操作,分別是-
    • reqreq.body 的物件可以取得 Request body 中的資料參數。
    • res:執行 res() 來回覆請求。
    • ctx:執行 ctx.json({...}) 組合回傳的資料,定義想要的格式和內容。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { rest } from 'msw'

export const handlers = [
rest.post('/login/:someData', (req, res, ctx) => {
const { username } = req.body

return res(
ctx.json({
id: 'f79e82e8-c34a-4dc7-a49e-9fadc0979fda',
username,
firstName: 'John',
lastName: 'Maverick',
})
)
}),
]

GraphQL

從 msw 匯入 graphqlgraphql 實作了兩種操作方式:

  • graphql.query():對應 GraphQL 的 Query 來完成資源的查詢。
  • graphql.mutation():對應 GraphQL 的 Mutation 來完成資源新增、刪除、修改等會產生副作用的操作。

這些方法需要固定傳入兩個參數:

  • name-已經定義好的 Query 或 Mutation 名稱。
  • resolver function-請求完成後的 resolve 函式。有三個參數可以操作,分別是-
    • reqreq.variables 的物件可以取得 Request 中的資料參數。
    • res:執行 res() 來回覆請求。
    • ctx:執行 ctx.data({...}) 組合回傳的資料,根據定義好的欄位格式,設計想要的內容。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { graphql } from 'msw'

export const handlers = [
// Capture a "Login" mutation
graphql.mutation('Login', (req, res, ctx) => {
const { username } = req.variables

return res(
ctx.data({
user: {
username,
id: 'f79e82e8-c34a-4dc7-a49e-9fadc0979fda',
firstName: 'John',
lastName: 'Maverick',
},
})
)
}),
]

當我們寫好 handlers 後,在 browser.ts(js) 中就可以新增負責初始化 MSW 的變數,並且傳入 handlers 作為初始化的參數, 讓 MSW 知道遇到哪些請求後要執行 mock API 的動作。

1
2
3
4
5
import { setupWorker } from 'msw';
import { handlers } from './handlers'

// This configures a Service Worker with the given request handlers.
export const worker = setupWorker(...handlers);

通常我們會在本機的開發環境,或是執行測試時,才會希望啟動 MSW。因此我們可以在主程式的進入點加上環境的條件判斷。

1
2
3
4
5
import { worker } from './mocks/browser';

if (isTesting || process.env.NODE_ENV === 'development') {
worker.start();
}

小結

針對不同專案找出最適合的 API 設計,以及讓前端可以在面對各種資料情況時,都能提供良好的介面與瀏覽體驗。這些事情都可以透過 Mock API 的過程中實現。希望透過今天的介紹,讓大家對於 Mock API 有更深的了解,並且感受 MSW 擁有的魅力與好處🥳

Reference

AC 關鍵字計畫 | API 開發、測試、除錯一次到位!使用 MSW 快速上手 Mock API

https://yuri-journal.me/軟體開發/2021081414/

作者

Yuri Tsai

發表於

2021-08-14

更新於

2023-01-16

許可協議

評論