在以前的兩篇教程中,咱們學會了如何去測試最簡單的 React 組件。在實際開發中,咱們的組件常常須要從外部 API 獲取數據,而且組件的交互邏輯也每每更復雜。在這篇教程中,咱們將學習如何測試更復雜的組件,包括用 Mock 去編寫涉及外部 API 的測試,以及經過 Enzyme 來輕鬆模擬組件交互javascript
初次嘗試 Jest Mock
咱們的應用程序一般須要從外部的 API 獲取數據。在編寫測試時,外部 API 可能因爲各類緣由而失敗。咱們但願咱們的測試是可靠和獨立的,而最多見的解決方案就是 Mock。html
改寫 TodoList 組件
首先讓咱們改造組件,使其可以經過 API 獲取數據。安裝 axios:前端
npm install axios
而後改寫 TodoList
組件以下:java
// src/TodoList.js
import React, { Component } from 'react';
import axios from 'axios';
import Task from './Task';
const apiUrl = 'https://api.tuture.co';
class ToDoList extends Component {
state = {
tasks: [],
};
componentDidMount() {
return axios
.get(`${apiUrl}/tasks`)
.then((tasksResponse) => {
this.setState({ tasks: tasksResponse.data });
})
.catch((error) => console.log(error));
}
render() {
return (
<ul>
{this.state.tasks.map((task) => (
<Task key={task.id} id={task.id} name={task.name} />
))}
</ul>
);
}
}
export default ToDoList;
TodoList
被改形成了一個「聰明組件」,在 componentDidMount
生命週期函數中經過 axios
模塊異步獲取數據。react
編寫 axios 模塊的 mock 文件
Jest 支持對整個模塊進行 Mock,使得組件不會調用原始的模塊,而是調用咱們預設的 Mock 模塊。按照官方推薦,咱們建立 mocks 目錄並把 mock 文件放到其中。建立 axios 的 Mock 文件 axios.js,代碼以下:ios
// src/__mocks__/axios.js
'use strict';
module.exports = {
get: () => {
return Promise.resolve({
data: [
{
id: 0,
name: 'Wash the dishes',
},
{
id: 1,
name: 'Make the bed',
},
],
});
},
};
這裏的 axios 模塊提供了一個 get
函數,而且會返回一個 Promise,包含預先設定的假數據。git
經過 spyOn 函數檢查 Mock 模塊調用狀況
讓咱們開始 Mock 起來!打開 TodoList 的測試文件,首先在最前面經過 jest.mock
配置 axios 模塊的 Mock(確保要在 import TodoList
以前),在 Mock 以後,不管在測試仍是組件中使用的都將是 Mock 版本的 axios。而後建立一個測試用例,檢查 Mock 模塊是否被正確調用。代碼以下:github
// src/TodoList.test.js
import React from 'react';
import { shallow, mount } from 'enzyme';
import axios from 'axios';
jest.mock('axios');
import ToDoList from './ToDoList';
describe('ToDoList component', () => {
// ...
describe('when rendered', () => {
it('should fetch a list of tasks', () => {
const getSpy = jest.spyOn(axios, 'get');
const toDoListInstance = shallow(<ToDoList />);
expect(getSpy).toBeCalled();
});
});
});
測試模塊中一個函數是否被調用其實是比較困難的,可是所幸 Jest 爲咱們提供了完整的支持。首先經過 jest.spyOn
,咱們即可以監聽一個函數的使用狀況,而後使用配套的 toBeCalled
Matcher 來判斷該函數是否被調用。總體代碼十分簡潔,同時也保持了很好的可讀性。web
若是你忘記了 Jest Matcher 的含義,推薦閱讀本系列的第一篇教程。npm
迭代 TodoList 組件
一個實際的項目總會不斷迭代,固然也包括咱們的 TodoList 組件。對於一個待辦事項應用來講,最重要的固然即是添加新的待辦事項。
修改 TodoList 組件,代碼以下:
// src/TodoList.js
// ...
class ToDoList extends Component {
state = {
tasks: [],
newTask: '',
};
componentDidMount() {
// ...
.catch((error) => console.log(error));
}
addATask = () => {
const { newTask, tasks } = this.state;
if (newTask) {
return axios
.post(`${apiUrl}/tasks`, { task: newTask })
.then((taskResponse) => {
const newTasksArray = [...tasks];
newTasksArray.push(taskResponse.data.task);
this.setState({ tasks: newTasksArray, newTask: '' });
})
.catch((error) => console.log(error));
}
};
handleInputChange = (event) => {
this.setState({ newTask: event.target.value });
};
render() {
const { newTask } = this.state;
return (
<div>
<h1>ToDoList</h1>
<input onChange={this.handleInputChange} value={newTask} />
<button onClick={this.addATask}>Add a task</button>
<ul>
{this.state.tasks.map((task) => (
<Task key={task.id} id={task.id} name={task.name} />
))}
</ul>
</div>
);
}
}
export default ToDoList;
因爲咱們大幅改動了 TodoList 組件,咱們須要更新快照:
npm test -- -u
若是你不熟悉 Jest 快照測試,請回看本系列第二篇教程。
更新後的快照文件反映了咱們剛剛作的變化:
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ToDoList component when provided with an array of tasks should render correctly 1`] = `
<div>
<h1>
ToDoList
</h1>
<input
onChange={[Function]}
value=""
/>
<button
onClick={[Function]}
>
Add a task
</button>
<ul />
</div>
`;
在測試中模擬 React 組件的交互
在上面迭代的 TodoList 中,咱們使用了 axios.post。這意味着咱們須要擴展 axios 的 mock 文件:
// src/__mocks__/axios.js
'use strict';
let currentId = 2;
module.exports = {
get: () => {
return Promise.resolve({
// ...
],
});
},
post: (url, data) => {
return Promise.resolve({
data: {
task: {
name: data.task,
id: currentId++,
},
},
});
},
};
能夠看到上面,咱們添加了一個
currentId
變量,由於咱們須要保持每一個 task 的惟一性。
讓咱們開始測試吧!咱們測試的第一件事是檢查修改輸入值是否更改了咱們的狀態:
咱們修改 app/components/TodoList.test.js
以下:
import React from 'react';
import { shallow } from 'enzyme';
import ToDoList from './ToDoList';
describe('ToDoList component', () => {
describe('when the value of its input is changed', () => {
it('its state should be changed', () => {
const toDoListInstance = shallow(
<ToDoList/>
);
const newTask = 'new task name';
const taskInput = toDoListInstance.find('input');
taskInput.simulate('change', { target: { value: newTask }});
expect(toDoListInstance.state().newTask).toEqual(newTask);
});
});
});
這裏要重點指出的就是 simulate[1] 函數的調用。這是咱們幾回提到的ShallowWrapper的功能。咱們用它來模擬事件。它第一個參數是事件的類型(因爲咱們在輸入中使用onChange,所以咱們應該在此處使用change),第二個參數是模擬事件對象(event)。
爲了進一步說明問題,讓咱們測試一下用戶單擊按鈕後是否從咱們的組件發送了實際的 post 請求。咱們修改測試代碼以下:
import React from 'react';
import { shallow } from 'enzyme';
import ToDoList from './ToDoList';
import axios from 'axios';
jest.mock('axios');
describe('ToDoList component', () => {
describe('when the button is clicked with the input filled out', () => {
it('a post request should be made', () => {
const toDoListInstance = shallow(
<ToDoList/>
);
const postSpy = jest.spyOn(axios, 'post');
const newTask = 'new task name';
const taskInput = toDoListInstance.find('input');
taskInput.simulate('change', { target: { value: newTask }});
const button = toDoListInstance.find('button');
button.simulate('click');
expect(postSpy).toBeCalled();
});
});
});
感謝咱們的 mock 和 simulate 事件,測試經過了!如今事情會變得有些棘手。咱們將測試狀態是否隨着咱們的新任務而更新,其中比較有趣的是請求是異步的,咱們繼續修改代碼以下:
import React from 'react';
import { shallow } from 'enzyme';
import ToDoList from './ToDoList';
import axios from 'axios';
jest.mock('axios');
describe('ToDoList component', () => {
describe('when the button is clicked with the input filled out, the new task should be added to the state', () => {
it('a post request should be made', () => {
const toDoListInstance = shallow(
<ToDoList/>
);
const postSpy = jest.spyOn(axios, 'post');
const newTask = 'new task name';
const taskInput = toDoListInstance.find('input');
taskInput.simulate('change', { target: { value: newTask }});
const button = toDoListInstance.find('button');
button.simulate('click');
const postPromise = postSpy.mock.results.pop().value;
return postPromise.then((postResponse) => {
const currentState = toDoListInstance.state();
expect(currentState.tasks.includes((postResponse.data.task))).toBe(true);
})
});
});
});
就像上面看到的,postSpy.mock.results 是 post 函數發送結果的數組,經過使用它,咱們能夠獲得返回的 promise,咱們能夠從 value
屬性中取到這個 promise。從測試返回 promise 是確保 Jest 等待其異步方法執行結束的一種方法。
小結
在本文中,咱們介紹了 mock 模塊,並將其用於僞造API調用。因爲沒有發起實際的 post 請求,咱們的測試能夠更可靠,更快。除此以外,咱們還在整個 React 組件中模擬了事件。咱們檢查了它是否產生了預期的結果,例如組件的請求或狀態變化。爲此,咱們瞭解了 spy 的概念。
嘗試測試 React Hooks
Hooks 是 React 的一個使人興奮的補充,毫無疑問,它能夠幫助咱們將邏輯與模板分離。這樣作使上述邏輯更具可測試性。不幸的是,測試鉤子並無那麼簡單。在本文中,咱們研究瞭如何使用 react-hooks-testing-library[2] 處理它。
咱們建立 src/useModalManagement.js
文件以下:
// src/useModalManagement.js
import { useState } from 'react';
function useModalManagement() {
const [isModalOpened, setModalVisibility] = useState(false);
function openModal() {
setModalVisibility(true);
}
function closeModal() {
setModalVisibility(false);
}
return {
isModalOpened,
openModal,
closeModal,
};
}
export default useModalManagement;
上面的 Hooks 能夠輕鬆地管理模式狀態。讓咱們開始測試它是否不會引起任何錯誤,咱們建立 useModalManagement.test.js
// src/useModalManagement.test.js
import useModalManagement from './useModalManagement';
describe('The useModalManagement hook', () => {
it('should not throw an error', () => {
useModalManagement();
});
});
咱們運行測試,獲得以下的結果:
FAIL useModalManagement.test.js
The useModalManagement hook
✕ should not throw an error按 ⌘+↩ 退出
不幸的是,上述測試沒法正常進行。咱們能夠經過閱讀錯誤消息找出緣由:
無效的 Hooks 調用, Hooks 只能在函數式組件的函數體內部調用。
讓測試經過
React文檔[3] 裏面提到:咱們只能從函數式組件或其餘 Hooks 中調用 Hooks。咱們可使用本系列前面部分介紹的 enzyme 庫來解決此問題,並且使了一點小聰明,咱們建立 testHook.js
:
// src/testHook.js
import React from 'react';
import { shallow } from 'enzyme';
function testHook(hook) {
let output;
function HookWrapper() {
output = hook();
return <></>;
}
shallow(<HookWrapper />);
return output;
}
export default testHook;
咱們繼續迭代 useModalManagement.test.js
,修改內容以下:
// src/useModalManagement.test.js
import useModalManagement from './useModalManagement';
import testHook from './testHook';
describe('The useModalManagement hook', () => {
it('should not throw an error', () => {
testHook(useModalManagement);
});
});
咱們容許測試,獲得以下結果:
PASS useModalManagement.test.js
The useModalManagement hook
✓ should not throw an error
好多了!可是,上述解決方案不是很好,而且不能爲咱們提供進一步測試 Hooks 的溫馨方法。這就是咱們使用 react-hooks-testing-library[4] 的緣由,咱們將在下一篇教程裏講解如何更加溫馨的測試 React Hooks 的方法,敬請期待!
參考資料
simulate: https://enzymejs.github.io/enzyme/docs/api/ShallowWrapper/simulate.html
[2]react-hooks-testing-library: https://wanago.io/2019/12/09/javascript-design-patterns-facade-react-hooks/
[3]React文檔: https://reactjs.org/docs/hooks-overview.html
[4]react-hooks-testing-library: https://wanago.io/2019/12/09/javascript-design-patterns-facade-react-hooks/
·END·
匯聚精彩的免費實戰教程
喜歡本文,點個「在看」告訴我

本文分享自微信公衆號 - 圖雀社區(tuture-dev)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。