在前端,我們會有好幾種呼叫 api 的方式:
- 直接利用 fetch 或 axios 等直接呼叫 api
- 將 fetch 和需要的參數封裝在一個 function 裏,用 function 來呼叫 api
此處我們用第二種方式處理,下面會說明原因
讓 call api 的時候更語意化,不用太過在乎細節,讓封裝過的 api function 來處理 call api 的相關細節
範例如下: 假設我們今天想上傳一個 user 的資訊
/* /apis/user.js */
const apiUploadUser = async (userFields) => {
const formData = new FormData();
Object.entries(userFields).forEach(([key, value]) => {
uploadForm.append(key, value);
});
await axios({
method: 'post',
url: '/users',
headers: { 'Content-Type': 'multipart/form-data' },
data: uploadForm,
});
};
/* /page/user.jsx */
const User = () => {
const [name, setName] = useState();
const [email, setEmail] = useState();
const [address, setAddress] = useState();
useEffect(() => {
apiUploadUser({ name, email, address })
.then(() => {
alert('Upload user successfully');
})
.catch(() => {
alert('Upload user failed')
});
}, []);
return (...);
};
這樣我們在 Component 呼叫時就不用寫太多邏輯了,將 api 處理邏輯的部份和 Component 處理 UI 互動的部分分離開來
因為我們是在做單元測試,基本上希望與外界的連結越少越好,才能確保每個工作單位測試的結果都能維持醫治,網路請求就是外部環境之一,所以,針對工作單位會使用到網路請求的部分,我們可以使用偽資料 (mock data),去除外部因素,使得單元測試能夠在穩定的 api response 下正確的顯示測驗結果
如果專案上使用封裝的 api,通常我們會將相同業務邏輯的資料放在一起,例如跟 user 有關的放一起,product 有關的放在一起
若專案上對應的後端架構是 micro service,可以直接以 service 來區分封裝 api 的資料夾,例如:
/api/user.js 、/api/product.js
假設我們在 /api/user.js 有 3 隻 api
export const apiGetUser = async ({ userUuid }) => {
await axios({
method: 'get',
url: '/users',
params: { uuid: userUuid },
});
};
export const apiUploadUser = async (userFields) => {
const formData = new FormData();
Object.entries(userFields).forEach(([key, value]) => {
uploadForm.append(key, value);
});
await axios({
method: 'post',
url: '/users',
headers: { 'Content-Type': 'multipart/form-data' },
data: uploadForm,
});
};
export const apiUpdateUser = async ({
userUuid,
...userFields,
}) => {
const formData = new FormData();
Object.entries(userFields).forEach(([key, value]) => {
uploadForm.append(key, value);
});
await axios({
method: 'post',
url: '/users',
params: { uuid: userUuid },
headers: { 'Content-Type': 'multipart/form-data' },
data: uploadForm,
});
};
若我們要 mock api 的 response,我們需要執行以下 3 步驟:
jest.mock
mock 整個檔案.mockResolvedValue
來 mock api response
引入真實封裝 api 的 function 的所在檔案
自動 mock,用 jest.mock mock 整個檔案
import { apiUploadUser, ... } from '/apis/users';
jest.mock('./users');
這時候,裡面的每個封裝的 api 都會被改寫成 jest.fn,我們就可以利用 jest.fn 的相關 function 來對封裝的 api function 做監控,包括:
手動 mock
如果我們封裝的 api 有一些特別的需求,不想要 jest.mock 直接幫我們複寫成 jest.fn, 我們可以在 jest.mock 傳入第 2 個參數,讓我們自己 mock 對應的 api, (如當我們封裝的 api 是用 class 撰寫時,就需要自己撰寫,如後續介紹)
import { apiUploadUsers, ... } from './users';
jest.mock('./users', () => ({
apiGetUser: jest.fn(),
apiUploadUser: jest.fn(),
apiUpdateUser: jest.fn(),
}));
對於已經用 jest.fn
覆寫過的 api function,我們可以用 mockResolvedValue
來偽造對應的 api response,這樣的好處是:
壞處是:
describe('Component', () => {
test('when under some conditions, should show responded results', () => {
apiGetUser.mockResolvedValue({
name: 'fake user name',
email: 'fake user email',
address: 'fake user address',
});
...
});
test('when under new conditions, should show another results', () => {
apiGetUser.mockResolvedValue({
name: 'new fake name',
email: 'new fake email',
address: 'new fake address',
});
...
});
})
jest.spyOn
當我們在測試 Component 時,有時候只需要用到極少數的 api function,這時候,我們可以用 jest.spyOn 去 mock 特定的 api function,不用手動去複寫整包 module,範例如下:
import * as userApis from '/apis/users';
jest.spyOn(userApis, 'apiGetUser');
describe('Component', () => {
test('when under some conditions, should show some results', () => {
apiGetUser.mockResolvedValue({
name: 'fake name',
email: 'fake email',
address: 'fake address',
});
...
});
});
我們在做單元測試,但工作單位有網路請求時,我們可以利用 jest.mock
和 3 步驟在單元測試中偽造 api response,分別是:
就可以順利的達成偽造 api 的目的,順利地做單元測試