扫二维码与项目经理沟通
我们在微信上24小时期待你的声音
解答本文疑问/技术咨询/运营咨询/技术建议/互联网交流
本篇文章为大家展示了JavaScript中如何使用Mock模拟模块并处理组件交互,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。
创新互联是一家专注于成都网站设计、网站制作与策划设计,徐闻网站建设哪家好?创新互联做网站,专注于网站建设10余年,网设计领域的专业建站公司;建站业务涵盖:徐闻等地区。徐闻做网站价格咨询:028-86922220
我们将学习如何测试更复杂的组件,包括用 Mock 去编写涉及外部 API 的测试,以及通过 Enzyme 来轻松模拟组件交互
我们的应用程序通常需要从外部的 API 获取数据。在编写测试时,外部 API 可能由于各种原因而失败。我们希望我们的测试是可靠和独立的,而最常见的解决方案就是 Mock。
首先让我们改造组件,使其能够通过 API 获取数据。安装 axios:
npm install axios
然后改写 TodoList
组件如下:
// 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 (
{this.state.tasks.map((task) => (
))}
);
}
}
export default ToDoList;
TodoList
被改造成了一个“聪明组件”,在 componentDidMount
生命周期函数中通过 axios
模块异步获取数据。
Jest 支持对整个模块进行 Mock,使得组件不会调用原始的模块,而是调用我们预设的 Mock 模块。按照官方推荐,我们创建 mocks目录并把 mock 文件放到其中。创建 axios 的 Mock 文件 axios.js,代码如下:
// 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,包含预先设定的假数据。
让我们开始 Mock 起来!打开 TodoList 的测试文件,首先在最前面通过 jest.mock
配置 axios 模块的 Mock(确保要在 import TodoList
之前),在 Mock 之后,无论在测试还是组件中使用的都将是 Mock 版本的 axios。然后创建一个测试用例,检查 Mock 模块是否被正确调用。代码如下:
// 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( );
expect(getSpy).toBeCalled();
});
});
});
测试模块中一个函数是否被调用实际上是比较困难的,但是所幸 Jest 为我们提供了完整的支持。首先通过 jest.spyOn
,我们便可以监听一个函数的使用情况,然后使用配套的 toBeCalled
Matcher 来判断该函数是否被调用。整体代码十分简洁,同时也保持了很好的可读性。
如果你忘记了 Jest Matcher 的含义,推荐阅读本系列的第一篇教程。
一个实际的项目总会不断迭代,当然也包括我们的 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 (
ToDoList
{this.state.tasks.map((task) => (
))}
);
}
}
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`] = `
ToDoList
onChange={[Function]}
value=""
/>
onClick={[Function]}
>
Add a task
`;
在上面迭代的 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(
);
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(
);
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(
);
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 的概念。
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( );
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] 的原因。
上述内容就是JavaScript中如何使用Mock模拟模块并处理组件交互,你们学到知识或技能了吗?如果还想学到更多技能或者丰富自己的知识储备,欢迎关注创新互联行业资讯频道。
我们在微信上24小时期待你的声音
解答本文疑问/技术咨询/运营咨询/技术建议/互联网交流