部署DeepSeek模型,进群交流最in玩法!
立即加群
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >JavaScript 测试系列实战(三):使用 Mock 模拟模块并处理组件交互

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

作者头像
一只图雀
发布于 2020-09-10 02:56:18
发布于 2020-09-10 02:56:18
5.2K11
代码可运行
举报
文章被收录于专栏:图雀社区图雀社区
运行总次数:1
代码可运行

在之前的两篇教程中,我们学会了如何去测试最简单的 React 组件。在实际开发中,我们的组件经常需要从外部 API 获取数据,并且组件的交互逻辑也往往更复杂。在这篇教程中,我们将学习如何测试更复杂的组件,包括用 Mock 去编写涉及外部 API 的测试,以及通过 Enzyme 来轻松模拟组件交互

初次尝试 Jest Mock

我们的应用程序通常需要从外部的 API 获取数据。在编写测试时,外部 API 可能由于各种原因而失败。我们希望我们的测试是可靠和独立的,而最常见的解决方案就是 Mock。

改写 TodoList 组件

首先让我们改造组件,使其能够通过 API 获取数据。安装 axios:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
npm install axios

然后改写 TodoList 组件如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 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 模块异步获取数据。

编写 axios 模块的 mock 文件

Jest 支持对整个模块进行 Mock,使得组件不会调用原始的模块,而是调用我们预设的 Mock 模块。按照官方推荐,我们创建 mocks 目录并把 mock 文件放到其中。创建 axios 的 Mock 文件 axios.js,代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 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,包含预先设定的假数据。

通过 spyOn 函数检查 Mock 模块调用情况

让我们开始 Mock 起来!打开 TodoList 的测试文件,首先在最前面通过 jest.mock 配置 axios 模块的 Mock(确保要在 import TodoList 之前),在 Mock 之后,无论在测试还是组件中使用的都将是 Mock 版本的 axios。然后创建一个测试用例,检查 Mock 模块是否被正确调用。代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 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 来判断该函数是否被调用。整体代码十分简洁,同时也保持了很好的可读性。

如果你忘记了 Jest Matcher 的含义,推荐阅读本系列的第一篇教程。

迭代 TodoList 组件

一个实际的项目总会不断迭代,当然也包括我们的 TodoList 组件。对于一个待办事项应用来说,最重要的当然便是添加新的待办事项。

修改 TodoList 组件,代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 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 组件,我们需要更新快照:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
npm test -- -u

如果你不熟悉 Jest 快照测试,请回看本系列第二篇教程。

更新后的快照文件反映了我们刚刚做的变化:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 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 文件:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 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 如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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 请求。我们修改测试代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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 事件,测试通过了!现在事情会变得有些棘手。我们将测试状态是否随着我们的新任务而更新,其中比较有趣的是请求是异步的,我们继续修改代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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 文件如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// src/useModalManagement.test.js
import useModalManagement from './useModalManagement';

describe('The useModalManagement hook', () => {
  it('should not throw an error', () => {
    useModalManagement();
  });
});

我们运行测试,得到如下的结果:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
FAIL useModalManagement.test.js
  The useModalManagement hook
    ✕ should not throw an error按 ⌘+↩ 退出

不幸的是,上述测试无法正常进行。我们可以通过阅读错误消息找出原因:

无效的 Hooks 调用, Hooks 只能在函数式组件的函数体内部调用。

让测试通过

React文档[3] 里面提到:我们只能从函数式组件或其他 Hooks 中调用 Hooks。我们可以使用本系列前面部分介绍的 enzyme 库来解决此问题,而且使了一点小聪明,我们创建 testHook.js

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 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,修改内容如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// src/useModalManagement.test.js
import useModalManagement from './useModalManagement';
import testHook from './testHook';

describe('The useModalManagement hook', () => {
  it('should not throw an error', () => {
    testHook(useModalManagement);
  });
});

我们允许测试,得到如下结果:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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 -

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-09-04,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 图雀社区 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
1 条评论
热度
最新
厉害
厉害
回复回复点赞举报
推荐阅读
编辑精选文章
换一批
微信小程序登录与注册验证码倒计时的效果实现
可以看到,我们在点击获取验证码以后,就开始倒计时了,正常都是从60s倒计时的,这里为了演示方便,我从6s开始的。可以看到倒计时结束后,按钮又恢复了可以点击的状态。
编程小石头
2020/10/22
2.2K0
微信小程序登录与注册验证码倒计时的效果实现
微信小程序 - 通用页面(登录、注册、找回密码)
点击登录时,动态设置data属性值,改变登录按钮文本,背景色,显示loading动画,不可点击
用户5997198
2019/09/02
19.4K1
微信小程序 - 通用页面(登录、注册、找回密码)
定时器setTimeout和setInterval的简单应用
本文简单利用定时器setTimeout和setInterval举了两个小栗子:定时炸弹和1-100递增
全栈程序员站长
2022/11/09
6440
JavaScript设置定时器、取消定时器及执行机制解析
今天整理了一下 JavaScript 定时器,顺便了解了一下 JavaScript 的运行机制,现在记录一下。
德顺
2019/11/12
5.1K0
JavaScript 定时器 setTimeout、setInterval
定时器在javascript中的作用 1、制作动画 2、异步操作 3、函数缓冲与节流 定时器类型及语法 示例代码如下: /* 定时器: setTimeout 只执行一次的定时器 clearTimeout 关闭只执行一次的定时器 setInterval 反复执行的定时器 clearInterval 关闭反复执行的定时器 */ var time1 = setTimeout(myalert,2000); var time2 = setInterval(
Devops海洋的渔夫
2019/05/30
1K0
微信小程序实现点击获取验证码倒计时结束可再点击功能
效果图 js Page({ data: { //点击前的文本内容 text: '发送验证码', //控制按钮能否点击 disabled: false, //倒计时时间 time: 60, //定时器 timer: '' }, //点击方法 send: function() { //将按钮设置为禁用 this.setData({ disabled: true }) //给定时器赋值 this.data.ti
peng_tianyu
2022/12/15
1.1K0
微信小程序实现点击获取验证码倒计时结束可再点击功能
小程序验证码倒计时
现在好多小程序都没有用到手机号的登录,因为可以直接调用微信的接口,getPhoneNumber,因为我们为了保持公众号,小程序,app后台的一致性,,又做了手机号的登录。 问题: 简单描述一下功能:输入手机号,点击获取验证码。我必须在点击那个获取验证按钮之前,在js中获取手机号。 如何获取到input提交之前的输入值呢? 3.小程序的收取短信的倒计时方法? 解决方法: 微信对input的组件,提供了多个事件,看来只能通过这些事件去实现 单个input的值的获取。bindblur ,失去焦点事件,e
honey缘木鱼
2018/06/13
1.8K0
微信小程序发送短信验证码完整实例
微信小程序注册完整实例,发送短信验证码,带60秒倒计时功能,无需服务器端。效果图:
用户4432598
2019/01/06
10.3K1
【黄啊码】微信小程序倒计时秒杀功能
在使用完定时器后一定要清除定时器,否则定时器将一直运行,占用程序资源,甚至程序报错。关于有效清除定时器方法在微信开放社区的讨论:微信小程序使用clearInterval清除定时函数无效? | 微信开放社区
黄啊码
2021/09/26
8140
JavaScript基础-定时器:setTimeout, setInterval
在JavaScript的世界里,定时器是实现异步编程不可或缺的工具,它允许我们按计划执行某些代码片段。setTimeout和setInterval作为两大核心定时器函数,广泛应用于页面动画、定时更新、延时操作等多种场景。本文将深入浅出地介绍这两个函数的基本用法、常见问题、易错点及避免策略,并通过代码示例加以说明。
Jimaks
2024/06/14
7100
JavaScript基础-定时器:setTimeout, setInterval
JavaScript——定时器
1. 定时器的介绍 定时器就是在一段特定的时间后执行某段程序代码。 2. 定时器的使用: js 定时器有两种创建方式: setTimeout(func[, delay, param1, param2, ...]) :以指定的时间间隔(以毫秒计)调用一次函数的定时器 setInterval(func[, delay, param1, param2, ...]) :以指定的时间间隔(以毫秒计)重复调用一个函数的定时器 setTimeout函数的参数说明: 第一个参数 func , 表示定时器要执行的函数名
落雨
2022/04/21
30K0
js中settimeout和setInterval区别_JavaScript set
注:调用过程中,可以使用clearTimeout(id_of_settimeout)终止
全栈程序员站长
2022/11/09
2.1K0
JavaScript定时器:setTimeout与setInterval 定时器与异步循环数组
深入了解一下 关于JavaScript定时器的知识; setTimeout与setInterval简述 setTimeout与setInterval使用方法基本相同,他们接受两个参数,第一个参数是需要执行的函数,第二个参数是执行的延迟时间,看栗子: setTimeout(function(){ alert("hello"); //第一个参数为函数 你可以传入函数名 或一个匿名函数 },3000);     //第二个参数为延迟时间 标识多少毫秒之后执行前一个函数 setInt
前朝楚水
2018/04/03
2.3K0
利用setTimeout和SetInterval构建Javascript计时器
看到了一篇深入浅出的讲解setTimeout和setInterval的例子,直接讲英文贴出来吧,也不是很难。
大江小浪
2018/07/24
8620
从零开始学 Web 之 BOM(二)定时器
多次点击“摇起来”按钮的时候,timeId 的值会有多个,而停止的时候,只会清理最后一个值,其他的值对应的定时器没有清理。
Daotin
2018/08/31
1.4K0
验证码倒计时的注册页面
原型图 需求:手机号验证 发送验证码之后开始60S倒计时 60s以后如果没有填写验证码,可重新发送 <!doctype html> <html> <head> <meta charset="utf-8"> <title>注册</title> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" con
王小婷
2018/06/01
1.2K0
微信小程序开发实战(29):控制背景音乐
小程序还提供一组用于播放背景音乐的API,背景音乐和普通音乐的区别就是背景音乐在当前页面播放后,即使切换到当前小程序的其他页面,也不会停止播放。但当小程序退出后,背景音乐就会停止播放。
蒙娜丽宁
2020/09/07
2.9K0
微信小程序开发实战(29):控制背景音乐
从一个超时程序的设计聊聊定时器的方方面面
企业项目开发中经常有这样一个逻辑场景:在界面上显示倒计时,时间到了给出提示,禁止用户操作。
LIYI
2022/03/08
1.5K0
从一个超时程序的设计聊聊定时器的方方面面
【如果你要学JS {十一}】——window常见事件,灵活运用定时器
BOM ( Browser Object Model )即浏览器对象模型,它提供了独立于内容而与浏览器窗口进行交互的对象,其核心 对象是window,BOM由一系列相关的对象构成,并且每个对象都提供了很多方法与属性。 BOM缺乏标准, JavaScript 语法的标准化组织是ECMA , DOM的标准化组织是W3C , BOM最初是Netscape浏 览器标准的一部分。
像素人
2023/12/24
1.1K0
【如果你要学JS {十一}】——window常见事件,灵活运用定时器
Vue中 使用定时器 (setInterval、setTimeout)[通俗易懂]
js中定时器有两种,一个是循环执行 setInterval,另一个是定时执行 setTimeout。
全栈程序员站长
2022/11/10
8.9K0
Vue中 使用定时器 (setInterval、setTimeout)[通俗易懂]
推荐阅读
相关推荐
微信小程序登录与注册验证码倒计时的效果实现
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验