今天来谈谈 Mock Module / Function 的部分。Mock Function 是在 Jest 中非常强大的功能,它可以模拟某个函式会回传的值,并且监控该函式被执行的次数,而 Mock Module 可以说是垫加在 Mock Function 上的功能,也就是可以直接模拟某套件中特定函式回传的结果。
虽然说在写测试时 Mock Module 很常会被使用到,但因为 Jest 提供了几种不同的方式让开发者能够 Mock Module,因此过去在找资料时常会看到多种不同写法而对 Mock 的方式感到有点混乱,这篇内容则是整理了笔者比较常用的方式。
在开始说明 Mock Module 的概念前,让我们先来看个例子。现在我们有个 React 元件长这样:
import { useEffect, useState } from 'react';
import { fetchUser } from './utils/api';
import { User } from './types';
function App() {
const [users, setUsers] = useState<User[]>([]);
useEffect(() => {
fetchUser<User[]>().then((users) => {
return setUsers(users);
});
}, []);
return (
<div className="App">
<header className="App-header">
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</header>
</div>
);
}
export default App;
在 <App />
中,会载入 ./utils/api
这支档案,在 ./utils/api
中则撰写一个用来 fetch User API 的方法,它会实际向 jsonplaceholder 发送请求:
// ./utils/api
export const fetchUser = async <T>() => {
const resp = await fetch('https://jsonplaceholder.typicode.com/users');
const data = (await resp.json()) as T;
return data;
};
预设的情况下,jsonplaceholder 会回传 10 个使用者的资料回来,这时候画面会长这样:
接着一如往常的来撰写测试,可以写成像这样:
// App.test.tsx
import React from 'react';
import { render, screen } from '@testing-library/react';
import App from './App';
import { create } from 'react-test-renderer';
test('render user data successfully', async () => {
render(<App />);
const listItemElements = await screen.findAllByRole('listitem');
expect(listItemElements.length).toBe(10);
});
在这个测试中我们透过 react-testing-library 提供的 render
来转译出 <App />
这个元件,接着在 DOM 上找出 <li />
的元素,并 assert 它应该有 10 个,测试也顺利的通过了:
这里笔者不会说明如何使用 react-testing-library 来撰写 React 元件的测试,如果有需要的话,推荐可以看 React Testing Library Tutorial @ Youtube。
但等等,这个测试实际上真的有去打 API 吗?你觉得呢?
我们可以在 <App />
元件中的 useEffect
中加上 console.log('[App] users', users);
,看看取得的资料是否和从 jsonplaceholder 回来的资料相同:
useEffect(() => {
fetchUser<User[]>().then((users) => {
console.log('[App] users', users);
return setUsers(users);
});
}, []);
当郎~的确是打 API 回来的资料:
从上面的例子中可以看到,我们真的对 jsonplaceholder 发出了请求,但等等,这样是不是表示我们的测试以後都需要连上网路才能执行?就算连网本身不是问题,未来如果 jsonplaceholder 的网站维修的话,是不是表示我们的测试也会连带无法进行。
除非是後期的 E2E 测试,否则当我们的测试依赖外部的资源时,是一件不太好的事,因为测试 failed 的时候,开发者将没办法马上知道现在测试 failed 的原因是我们的程序写坏了,还是外部的网站挂了。逻辑上同样的程序码每次执行测试得到的结果应该要是稳定的,不会现在跑没事,下次跑却莫名失败。
未来确保执行测试时能够控制不要让外部的变数影响到测试的稳定度,我们可以透过 Jest 的 Mock Function 来模拟 API 回传的结果,让这个元件不需要实际发送请求。
实际上,不只是串接 API 才需要使用到 Mock Function,如果要测试的内容包含会持续变动的时间、路由(router)、custom hooks 等等,也都可也透过 Mock Function 模拟。
在撰写 Mock Module 的时候,开发者要清楚知道现在要 Mock 的 Function 是什麽,以上面的例子来说,我们想要模拟的应该是 fetchUser
这个方法:
因为只要能模拟 fetchUser
回传的结果的话,就不需要实际向 jsonplaceholder 发送 API 请求。
要模拟 fetchUser
,只需要在测试档(App.test.tsx
)中,加上这两行:
载入要被 mock 的 module(虽然在 test 档中不会用到)
使用 jest.mock(<module 的路径或名称>)
来 mock 它
// App.test.tsx
// 1. 载入要被 mock 的 module(虽然在 test 档中不会用到)
import { fetchUser } from './utils/api';
// 2. 使用 `jest.mock(<module 的路径或名称>)` 来 mock 它
jest.mock('./utils/api');
test('render user data successfully', async () => {
// ...
});
在使用了 jest.mock('./utils/api')
後,现在的这个 fetchUser
已经不是原本的 fetchUser
了,而是 Jest 的 mockFn(即 jest.fn()
),。因此就可以使用 Jest mockFn 提供的许多方法。举例来说,这里可以使用 mockResolvedValue
来模拟 fetchUser 会回传的内容:
test('render user data successfully', async () => {
// 模拟 fetchUser 会回传的内容
fetchUser.mockResolvedValue([
{
id: 1,
name: 'Leanne Graham',
username: 'Bret',
email: '[email protected]',
},
{
id: 2,
name: 'Ervin Howell',
username: 'Antonette',
email: '[email protected]',
},
]);
render(<App />);
const listItemElements = await screen.findAllByRole('listitem');
expect(listItemElements.length).toBe(2);
});
最神奇的地方就在这里了!这点非常非常重要!!
读者可能会觉得很奇怪,我们是在测试档 App.test.tsx
中汇入并模拟 fetchUser
这个方法的回传值,但实际上会影响到的是在 App
中 fetchUser
被呼叫到时的回传值。什麽意思,刚刚我们曾经在 App.tsx
中使用 console.log()
检视 fetchUser 回传的结果,我们来看看这时候 useEffect 中 fetchUser 回传的结果是什麽:
你会发现虽然是在 App.test.tsx
中 mock 了 fetchUser 这个方法,但实际上造成效果的地方会是在 App.tsx
中。这时候因为我们已经 mock 了 fetchUser 这个函式,所以 fetchUser 就不会再真的去对 jsonplaceholder 发送 API 请求。
透过 Mock Module/Function 的使用,就可以确保测试的结果不会受外部服务而有影响。
如果有在使用 TypeScript 的话,你可能会发现到测试档中在使用 mockFn 的地方 TypeScript 会报错:
报错的原因在於虽然我们用 jest.mock()
来让 fetchUser
变成了一个 mockFn,逻辑上 fetchUser 可以使用 mockFn 中的所有方法,但因为 TypeScript 并不知道 fetchUser 已经变成了 mockFn 这件事,因此会报错。
要解决这个问题,只需把 fetchUser
使用 Type Assertion 指定成 jest.Mock
或 jest.MockedFunction<typeof fetchUser>
即可,通常可以写成像这样:
import { fetchUser } from './utils/api';
jest.mock('./utils/api');
const mockedFetchUser = fetchUser as jest.MockedFunction<typeof fetchUser>; // 也可以 as jest.Mock
test('render user data successfully', async () => {
mockedFetchUser.mockResolvedValue([
/* ... */
]);
// ...
});
这时候因为有做 Type Assertion 的关系,在使用 mockedFetchUser
时就不会有 TypeScript 的型别错误。
此外,为了节省每次都需要使用 as jest.MockedFunction<...>
的写法,有开发者包好了一个 mockFunction
的 utility:
// credit and refer to:
// https://instil.co/blog/typescript-testing-tips-mocking-functions-with-jest/
export function mockFunction<T extends (...args: any[]) => any>(
fn: T
): jest.MockedFunction<T> {
return fn as jest.MockedFunction<T>;
}
使用时只需要变成:
import { fetchUser } from './utils/api'; // 要被 mock 的 module
import mockFunction from './utils/mockFunction';
jest.mock('./utils/api'); // 让该 module 变成 mockFn
const mockedFetchUser = mockFunction(fetchUser); // 处理 TS 型别
test('render user data successfully', async () => {
mockedFetchUser.mockResolvedValue([
/* ... */
]); // 建立 mock data
// ...
});
就完成了。
需要在「测试档」中撰写 mock module 和 mock data,实际的效果会作用在「原程序」中。
fetchUser
import { fetchUser } from './utils/api'
jest.mock<path_or_name>
来 mock 该 modulemockedFetchUser.mockResolvedValue
上一篇我们的基因体时代-AI, Data和生物资讯 Day20-注释基因资讯的BED档案格式和bed...
昨天跟大家介绍完middleware的基本建立後,大家有没有理解并成功做出来呢! 如果对於这部分还有...
目前我们已经做出台股加权指数的 K 线图,但目前进度的线图的 x 轴没有时间,所以当使用者看到这张图...
当开始可以发现韩国女团,每个人长得都不一样时,就代表你长大了... Climbing Stairs ...
简介 casting 就是资料型态之间的转换。 例如把 A type 转换成 B type。 但是这...