JavaScript 测试系列实战(三):使用 Mock 模拟模块并处理组件交互


在以前的两篇教程中,咱们学会了如何去测试最简单的 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: [
        {
          id0,
          name'Wash the dishes',
        },
        {
          id1,
          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 的方法,敬请期待!

参考资料

[1]

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 -
    

● JavaScript 测试系列实战(一):使用 Jest 和 Enzyme 测试 React 组件

● 你不知道的 Npm(Node.js 进阶必备好文)

● 用动画和实战打开 React Hooks(一):useState 和 useEffect



·END·

图雀社区

汇聚精彩的免费实战教程



关注公众号回复 z 拉学习交流群


喜欢本文,点个“在看”告诉我

本文分享自微信公众号 - 图雀社区(tuture-dev)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。

相关文章
相关标签/搜索