­

JavaScript测试教程–part 4:模拟 API 调用和模拟 React 组件交互[每日前端夜话0xEA]

  • 2019 年 11 月 25 日
  • 筆記

正文共:1822 字

预计阅读时间:6 分钟

作者:Marcin Wanago

翻译:疯狂的技术宅

来源:wanago.io

今天,我们进一步测试 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 的某些核心模块(例如 fspath ),则需要在模拟文件中明确调用 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) =&gt; {      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 组件中模拟了事件,并检查了它是否产生了预期的结果,例如组件的请求或状态变化,并且了解了监视的概念。

原文:https://wanago.io/2018/09/17/javascript-testing-tutorial-part-four-mocking-api-calls-and-simulation-react-components-interactions/

下面夹杂一些私货:也许你和高薪之间只差这一张图

愿你有个好前程,愿你月薪30K。我们是认真的 !

在公众号内回复“体系”查看高清大图