作者:Marcin Wanago翻译:疯狂的技术宅
原文:https://wanago.io/2018/09/17/...
未经允许严禁转载
- 1.JavaScript测试教程-part 1:用 Jest 进行单元测试
- 2.JavaScript测试教程–part 2:引入 Enzyme 并测试 React 组件
- 3.JavaScript测试教程–part 3:测试 props,挂载函数和快照测试
- 4.JavaScript测试教程–part 4:模拟 API 调用和模拟 React 组件交互
今天,我们进一步测试 React 组件。它涉及模拟组件交互和模拟 API 调用。你将学到两种方法,开始吧!
模拟
对于我们的程序来说,从 API 获取一些数据是很常见的。但是它可能由于各种原因而失败,例如 API 被关闭。我们希望测试可靠且独立,并确保可以模拟某些模块。我们把 ToDoList 组件修改为智能组件。
app/components/ToDoList.component.js
import React, { Component } from 'react';
import Task from "../Task/Task";
import axios from 'axios';
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 (
<div>
<h1>ToDoList</h1>
<ul>
{
this.state.tasks.map(task =>
<Task key={task.id} id={task.id} name={task.name}/>
)
}
</ul>
</div>
)
}
}
export default ToDoList;
它使用 axios 提取数据,所以需要模拟该模块,因为我们不希望发出实际的请求。此类模拟文件在 mocks 目录中定义,在该目录中,文件名被视为模拟模块的名称。
__mocks__/axios.js
'use strict';
module.exports = {
get: () => {
return Promise.resolve({
data: [
{
id: 0,
name: 'Wash the dishes'
},
{
id: 1,
name: 'Make the bed'
}
]
});
}
};
如果你要模拟 Node 的某些核心模块(例如 fs 或 path ),则需要在模拟文件中明确调用 jest.mock('moduleName')
Jest 允许我们对函数进行监视:接下来测试是否调用了我们所创建的 get 函数。
app/components/ToDoList.test.js
import React from 'react';
import { shallow } from 'enzyme';
import ToDoList from './ToDoList';
import axios from 'axios';
jest.mock('axios');
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.mock('axios')
,Jest 在的测试和组件中都用我们的模拟代替了 axios。
spyOn 函数返回一个 mock函数。有关其功能的完整列表,请阅读文档。我们的测试检查组件在渲染和运行之后是否从模拟中调用 get函数,并成功执行。
PASS app/components/ToDoList/ToDoList.test.js
ToDoList component
when rendered
✓ should fetch a list of tasks
如果你在多个测试中监视模拟函数,请记住清除每个测试之间的模拟调用,例如通过运行 getSpy.mockClear()
,否则函数调用的次数将在测试之间保持不变。你还可以通过在 package.json 文件中添加以下代码段来使其成为默认行为:
"jest": {
"clearMocks": true
}
模拟获取 API
另一个常见情况是使用 Fetch API。一个窍门是它是附加到 window 对象的全局函数并对其进行模拟,可以将其附加到 global 对象。首先,让我们创建模拟的 fetch 函数。
__mock__/fetch.js
export default function() {
return Promise.resolve({
json: () =>
Promise.resolve([
{
id: 0,
name: 'Wash the dishes'
},
{
id: 1,
name: 'Make the bed'
}
])
})
}
然后,将其导入 setupTests.js 文件中。
app/setupTests.js
import Adapter from 'enzyme-adapter-react-16';
import { configure } from 'enzyme';
import fetch from './__mocks__/fetch';
global.fetch = fetch;
configure({adapter: new Adapter()});
注意,你需要在 package.json 中提供指向 setupTests.js 文件的路径——它在本教程的第二部分中进行了介绍。
现在你可以在组件中自由使用 fetch 了。
componentDidMount() {
return fetch(`${apiUrl}/tasks`)
.then(tasksResponse => tasksResponse.json())
.then(tasksData => {
this.setState({
tasks: tasksData
})
})
.catch(error => {
console.log(error);
})
}
设置监视时,请记住将其设置为 window.fetch
app/components/ToDoList.test.js
describe('ToDoList component', () => {
describe('when rendered', () => {
it('should fetch a list of tasks', () => {
const fetchSpy = jest.spyOn(window, 'fetch');
const toDoListInstance = shallow(
<ToDoList/>
);
expect(fetchSpy).toBeCalled();
});
});
});
模拟 React 组件的交互
在之前的文章中,我们提到了阅读组件的状态或属性,但这是在实际与之交互时。为了说明这一点,我们将增加一个把任务添加到 ToDoList 的功能。
app/components/ToDoList.js
import React, { Component } from 'react';
import Task from "../Task/Task";
import axios from 'axios';
class ToDoList extends Component {
state = {
tasks: [],
newTask: '',
}
componentDidMount() {
return axios.get(`${apiUrl}/tasks`)
.then(taskResponse => {
this.setState({
tasks: taskResponse.data
})
})
.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;
如你所见,我们在此处使用了 axios.post。这意味着我们需要扩展 axios 模拟。
__mocks__/axios.js
'use strict';
let currentId = 2;
module.exports = {
get: (url) => {
return Promise.resolve({
data: [
{
id: 0,
name: 'Wash the dishes'
},
{
id: 1,
name: 'Make the bed'
}
]
});
},
post: (url, data) {
return Promise.resolve({
data: {
task: {
name: data.task,
id: currentId++
}
}
});
}
};
我介绍 currentId 变量的原因是想保持ID唯一
首先检查修改输入值是否会改变我们的状态。
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 函数调用。它是前面提到过的 ShallowWrapper 的功能。我们用它来模拟事件。第一个参数是事件的类型(由于在输入中使用了 onChange,因此在这里应该用 change),第二个参数是模拟事件对象。
为了更进一步,让我们测试一下用户单击按钮后是否从的组件发送了实际的请求。
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();
});
});
});
测试通过了!
现在事情会变得有些棘手。我们将要测试状态是否能够随着的新任务而更新。有趣的是请求是异步的。
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 是能够确保 Jest 等待其解决的一种方法。
总结
在本文中,我们介绍了模拟模块,并将其用于伪造 API 调用。由于没有发出实际的请求要求,我们的测试可以更可靠、更快。除此之外,我们还在整个 React 组件中模拟了事件,并检查了它是否产生了预期的结果,例如组件的请求或状态变化,并且了解了监视的概念。
本文首发微信公众号:前端先锋
欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章
欢迎继续阅读本专栏其它高赞文章:
- 深入理解Shadow DOM v1
- 一步步教你用 WebVR 实现虚拟现实游戏
- 13个帮你提高开发效率的现代CSS框架
- 快速上手BootstrapVue
- JavaScript引擎是如何工作的?从调用栈到Promise你需要知道的一切
- WebSocket实战:在 Node 和 React 之间进行实时通信
- 关于 Git 的 20 个面试题
- 深入解析 Node.js 的 console.log
- Node.js 究竟是什么?
- 30分钟用Node.js构建一个API服务器
- Javascript的对象拷贝
- 程序员30岁前月薪达不到30K,该何去何从
- 14个最好的 JavaScript 数据可视化库
- 8 个给前端的顶级 VS Code 扩展插件
- Node.js 多线程完全指南
- 把HTML转成PDF的4个方案及实现
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。