React单元测试,就是把React 组件渲染出来,看看渲染出来的内容符不符合我们的预期。比如组件加载的时候有loading, 那就渲染组件,看看渲染出的内容中有没有loading. 再比如,ajax请求完成后,组件要显示返回的数据, 那就渲染组件, 等待请求完成,然后看看渲染出来内容是不是请求返回的数据。那怎么渲染?怎么查看渲染出来内容呢?因为我们是在命令行中跑测试,而不是在浏览器中进行测试, 渲染使用@testing-library/react, 提供了渲染方法。查看内容,则是Jest内置了jsdom, jsdom提供了DOM的无头实现,也就是说在命令行中跑测试,在测试中仍然可以获取到document, document.body 等DOM 元素,也就可以使用documet.getElementId() 等DOM 方法来查出内容,也可以click 来测试浏览器的交互形为。jsdom也称为Jest浏览器环境。

  @testing-library/react也鼓励我们,写测试要把注意力放到用户身上,测试要模拟真实用户的形为,而不是测试组件的实现细节,这样,测试完成后,对组件更有信心。什么是测试实现细节呢?就是测试组件状态是不是对的,直接调用组件中的方法。如果你了解Enzyme的话,它就提供了wrapper.state方法,可以直接获取到组件的状态,wapper.instance可以直接调用组件的方法。为什么不测试组件的实现细节?测试组件的实现细节有什么不好吗?有两个不好的地方,一是使用组件时,谁管你内部是怎么实现的,用户只管好不好用,有没有达到预期效果,测试实现细节显得没有什么意义。二是维护成本太高,今天组件的状态叫curValue, 明天可能叫currentValue, 这样测试就要改来改去,但这样的修改对组件来说,功能没有受到任何的影响,按理说,测试是不需要改的。@testing-library/react并没有提供测试实现细节的功能,只提供了getByText()等测试dom的功能。

  自己配置一个测试环境,稍微有点麻烦,create-react-app内置了@test-library/react,使用它创建项目,可以直接写测试。使用create-react-app 创建项目后,就可以看一下@test-library/react库了。React单元测试,就是渲染,查找元素,进行判断,是不是符合预期,也就是断言。React test libaray 提供了render()方法进行渲染,它接受一个React element, 然后把它渲染成DOM, 插入到body元素上。提供了*Text()等方法来查找元素,jest-dom提供了断言。写一个简单的例子,组件加载的时候显示loading, 然后请求数据, 展示数据。App.js修改如下

import React from 'react';

export default class App extends React.Component {
state = { todo: null}
componentDidMount() {
fetch('https://jsonplaceholder.typicode.com/todos/1')
.then(res => res.json())
.then(todo => this.setState({todo}))
.catch(e => console.log(e));
} render() {
return (
<React.Fragment>
{
this.state.todo ?
<p className="title">title: {this.state.todo.title}</p>
: <p className="spinner">loading</p>
}
</React.Fragment>
)
}
}

  测试的第一种情况是组件有没有显示 loading, 这和用户的形为是一致的。浏览器先渲染loading, 再渲染todo的title。用户先看到的是loading, 再看到的是title。按照测试步骤,先渲染组件,再查找loading ,最后断言其存在,如果测试通过,就表示组件功能没有问题。渲染直接调用render()方法,查找则要用@testing-library/react提供的screen对象,断言就用toBeInTheDocument()。 App.test.js 修改如下

import React from 'react';
import { render, screen } from '@testing-library/react';
import App from './App'; describe('app test', () => {
test('render a loading when component shows', () => {
render(<App></App>);
const hello = screen.getByText(/loading/);
expect(hello).toBeInTheDocument();
});
});

  自己配置一个Jest环境,稍微有点麻烦,mkdir react-test && cd react-test && npm init -y 新建项目react-test,npm install jest @testing-library/react --save-dev, 还要npm install react react-dom 安装react react-dom,由于node并不支持jsx,还要安装babel, @babel/core, @babel/preset-env, @babel/preset-react, 并配置.babelrc

{
"presets": ["@babel/preset-env", "@babel/preset-react"]
}

  Jest 27版本,test environment默认值是node,而不是jest dom, 所以还要配置test environment,jest.config.js

module.exports = {
testEnvironment: 'jsdom'
}

  写一个Hello.js

import React from "react"
export function Hello(){
return <div>Hello</div>
}

  新建Hello.test.js测试一下

import React from 'react';
import { render, screen } from '@testing-library/react';
import { Hello } from './Hello'; test('Hello', () => {
render(<Hello></Hello>);
const hello = screen.getByText(/Hello/);
expect(hello).toBeTruthy();
})

  npx jest, 测试成功,expect(hello).toBeTruthy()表明Hello 存在,但是语义不太清晰,如果toBeInTheDocument() 就好了,那要安装@testing-library/jest-dom, npm install --save-dev @testing-library/jest-dom ,然后在测试中import '@testing-library/jest-dom',由于在每一个测试中都要写这个配置比较麻烦,Jest 有一个配置项setupFilesAfterEnv,是一个路径数组,如果把某个文件所在的路径放到这个数组中,那么在跑测试之前Jest都会先运行这些文件中内容,路径数组中的各个文件相当于对Jest 进行了初始配置。在跑每个测试之前,都要配置jest-dom, 所以把jest-dom的配置文件放到setupFilesAfterEnv 中,那就要配置setupFilesAfterEnv。jest.config.js

module.exports = {
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['<rootDir>/jest-setup.js']
}

  再在项目根目录下建jest-setup.js

import '@testing-library/jest-dom'

  把Hello.test.js中import jest-dom去掉,npx jest, 测试成功。

  npx jest, 报错了,ReferenceError: fetch is not defined,为什么呢?因为跑单元测试,实际上是用node.js来执行单元测试的代码(JS代码)。在代码中,render()渲染组件,调用了组件的componentDidMount() 方法,在componentDidMount() 方法中,调用了fetch去请求数据,node.js中并没有fetch,所以报错了。现在只能由我们自己提供fetch了。提供fecth,直接给window对象添加fetch属性,因为现在是jest-dom环境, 在测试中,并不是真正地去请求数据,要mock fetch. 由于刚开始请求,请求是一种promise的pedding状态,所以直接返回一个promise 对象就可以了。

test('render a loading when component shows', () => {
window.fetch = jest.fn(() => {
return new Promise((resolve, reject) => { });
})
render(<App></App>);
const hello = screen.getByText(/loading/);
expect(hello).toBeInTheDocument();
});

 

 

  看一下React组件的执行过程,state.todo是null,render 方法返回<p className="spinner">loading</p>, 然后调用componentDidMount() 方法,fetch去请求数据(异步的),而在测试中,渲染出组件以后,直接断言了(同步),并没有等待fetch请求回来,fetch 在请求的过程中,测试已结束,所以渲染出的wrapper 只包含<p className="spinner">loading</p>, 测试通过,可以console.log(wrapper.debug()) 看组件的渲染结果。这也引出了测试的第二种情况,等待fetch请求结束,看返回的数据有没有正确渲染出来。有两个问题需要解决,一个是fetch, 在测试中,不是真正地去请求数据,所以要对fetch 进行mock.  fetch是window 对象的属性,所以对fetch的mock, 就是让window.fetch = jest.fn()。 jest.fn接受一个函数作为参数,函数的返回值就是mock函数的返回值。fetch的mock如下

window.fetch = jest.fn(() => {
return Promise.resolve({
status: 200,
json: () => {
return Promise.resolve({"title": "delectus aut autem"
})
}
})
})

  一个是等待, 等待fetch请求成功。等待用的是Jest测试的done参数,只要一个test测试中,参数有done, Jest 在测试的时候,就会等待这个done 的调用,如果done 不调用,Jest就会停在这个测试中。那么现在的问题变成了什么时候调用done(). fetch 返回的是promise, 所有注册的回调函数都放到异步队列中。异步队列的执行是node 的事件循环机制,还是无法知道,所有的回调函数什么时候执行完,什么时候调用done()。 但我们可以注册一个回调函数,只要保证fetch中注册的回调函数都执行完了,再执行我们注册的回调函数就可以了。这让我想到了setTimeout(), 同一个事件循环中,promise中的回调函数会在setTimeOut中的回调函数之前执行,那就在setTimeout 中调用done 就可以了

test('should render todo.title when fetch successfully ', (done) => {
window.fetch = jest.fn(() => {
return Promise.resolve({
status: 200,
json: () => {
return Promise.resolve({"title": "delectus aut autem"
})
}
})
}) const wrapper = shallow(<App></App>); setTimeout(() => {
expect(wrapper.find("p.title").text()).toContain("delectus");
expect(wrapper.find(".spinner").length).toBe(0);
done();
}, 10);
});
});

  看一下执行顺序,shallow(<App></App>)  -> componentDidMount() 执行 -> fetch发送请求,由于fetch是异步的,所以 shallow(<App></App>); 这一行代码算是执行完了,但由于mock, fetch立即resolve了,在执行下一行代码代码之前,fetch注册的回调函数已到异步队列中。再执行setTimeout, 告诉node, 10ms 之后注册断言的回调函数。顺序执行完毕,开始执行队列。执行res => res.json(),setState(), React 重新渲染,10ms 之后,断言的回调函数注册并执行,由于也执行了done() 测试结束,这时测试的就是fetch 返回数据之后的组件内容。注意,这里的setTimeout的延迟10s 只是举例,真正起作用的是事件循环队列的micro-task 和macro-task。promise是micro-task, setTimeout是micro-task。

  两种情况都通过测试,这个React组件就算测试完成了,因为它只有这两种情况。再稍微延伸一下,有人使用fetch的时候喜欢return

 componentDidMount() {
return fetch('https://jsonplaceholder.typicode.com/todos/1')
.then(res => res.json())
.then(todo => this.setState({todo}))
.catch(e => console.log(e));
}

  或有人喜欢使用async/await

    async componentDidMount() {
try {
const res = await fetch('https://jsonplaceholder.typicode.com/todos/1');
const todo = await res.json();
this.setState({todo})
} catch (e) {
console.log(e);
}
}

  这时componentDidMount() 调用的时候,就会返回promise, 在测试的时候,给这个promise注册回调函数,在回调函数里面时进行测试,可以保证fetch请求结束再进行断言。这时,你就想手动调用componentDidMount().  在shallow 渲染下是可以的,它接受第二个参数,是个对象,对shallow进行配置,disableLifecycleMethods: true, 表示渲染组件的时候不会调用生命周期函数。它返回的wrapper 有一个instance() 方法,返回react 实例,用它调用componentDidMount(), 测试内容修改如下, 还有一点要注意,在mock数据使用完成之后,最好把mock的函数进行还原。

test('should render todo.title when fetch successfully ', (done) => {
window.fetch = jest.fn(() => {
return Promise.resolve({
status: 200,
json: () => {
return Promise.resolve({"title": "delectus aut autem"
})
}
})
}) const wrapper = shallow(<App></App>, {disableLifecycleMethods: true});
let didMount = wrapper.instance().componentDidMount(); didMount.then(() => {
expect(wrapper.find("p.title").text()).toContain("delectus");
expect(wrapper.find(".spinner").length).toBe(0);
console.log("ues");
fetch.mockClear(); // mock 还原
done();
})

  fetch 还有一种使用情况,对其进行封装,新建一个request.js 文件,定义一个getData()

export function getData(url) {
return fetch(url).then(res => res.json())
}

  在App.js 中就要引入getData, 然后compentDidMount() 中使用它

    componentDidMount() {
return getData('https://jsonplaceholder.typicode.com/todos/1')
.then(todo => this.setState({todo}))
.catch(e => console.log(e));
}

  现在要怎么测试呢?组件依赖了另外一个模块,如果不想受这个模块影响,那就mock 这个模块。jest.mock() 一个模块,这个模块暴露出来的函数都变成了mock 函数,再从这个模块中引入函数,引入的都是mock函数,mock函数就可以mock实现,返回值等。

jest.mock('./request.js');
import { getData } from './request';

  这时,你会发现两个测试都报错了。第一个测试,shallow并没有禁止调用生命周期函数,compentDidMount会调用getData(), getData() 返回的是jest.fn() 没有then, 所以报错了,第二个也是如此。第一个可以禁止生命周期函数的调用。

 const wrapper = shallow(<App></App>, {disableLifecycleMethods: true});

   第二个对getData mock 实现

getData.mockResolvedValue({
    "title": "delectus aut autem",
})

  测试通过。这时,两个测试中都有const wrapper = shallow(<App></App>, {disableLifecycleMethods: true}); 可以进行抽取,因为在每一个测试之前都会shallow, 所以使用beforeEach()。mock的还原可以使用afterEach()

import React from 'react';
import { shallow } from 'enzyme';
import App from './App';
jest.mock('./request.js');
import { getData } from './request'; describe('app test', () => { let wrapper;
beforeEach(() => {
wrapper = shallow(<App></App>, {disableLifecycleMethods: true});
});
afterEach(() => {
getData.mockClear(); // mock 还原
}); test('render a loading when component shows', () => {
expect(wrapper.find('.spinner').exists()).toBeTruthy();
}); test('should render todo.title when fetch successfully ', (done) => {
getData.mockResolvedValue({
"title": "delectus aut autem",
})
let didMount = wrapper.instance().componentDidMount(); didMount.then(() => {
expect(wrapper.find("p.title").text()).toContain("delectus");
expect(wrapper.find(".spinner").length).toBe(0);
done();
})
});
});

  这里要注意,每个test之间要相互独立,不要使用共享数据,尤其是使用window 和document 对象的时候。当把变量提升到test 外面,放到describe中的时候,使用这个变量之前,一定要先进行赋值操作,可以使用beforeEach, 也可以在每一个test 的第一句,每一个test 都要使用它自己创建的变量。

  在form表单中,label的for 标签,对应input 的id 属性,点击label标签,input就会选中,focus。选择label标签,就能选中input标签,因为它们是一对。user.click(label), user.keybord('asss) 输入内容。user中@react/user-event 库 的export default

当渲染完组件后,如果不知道怎么选择元素,可以使用screen.logTestingPlaygroundURL,  测试跑起来,终端生成一个url, 把url 输入到浏览器中,上面左侧显示,渲染成html元素,右侧相当于显示页面的样子,选择哪个元素,下面就会显示推荐。如果右侧的元素不好选择,可以在左侧html中添加style样式,比如margin, padding 让元素更容易选择一下。

  如果使用getRole 不好找元素,可以给 元素一个testId, 使用getByTestId.  @react-test/libaray 还提供了within(), 接受查到的元素作为参数,表示,在元素下面进行查询。还有一种是直接使用dom的查询方式,querySelector.  render() 方法会返回 container.  container.querySelect

  findBy 返回的是promise, 所以要 expect(await findby).tobeIndocument()

act() warning: 就是unepected state updates

  点击button

  act 函数定义了时间窗口,状态的更新应该在里面发生。

但在react test-libary里面你不会手动调用act

user.keyboard和user.click 是同步的,也就是说,当点击之后,会更新状态(setstate)。如果调用了promise,可以需要find和waitfor。 为了解决act warning, 通常 用findby, 而不是直接调用act函数。

  可以看到有什么状态发生了改变,页面上多出了什么元素,直接await 这个元素就可以了。

  findby 等待 data fetch 回来,然后再获取元素,做测试

  当axios 和fetch 的时候,可以mock 服务器的api,msw 库,拦截请求,返回mock数据。

// src/mocks/server.js

import { setupServer } from 'msw/node'

import { handlers } from './handlers'

// This configures a request mocking server with the given request handlers.

export const server = setupServer(...handlers)
import { server } from './mocks/server.js'

// Establish API mocking before all tests.

beforeAll(() => server.listen())

// Reset any request handlers that we may add during the tests,

// so they don't affect other tests.

afterEach(() => server.resetHandlers())

// Clean up after the tests are finished.

afterAll(() => server.close())

  如果使用create-react-app,可以调置debug 命令, 在package.json中的script 中:"test:debug": "react-scripts --inspect-brk test --runInBand --no-cache". 然后在测试文件中, 需要debug的地y方加debugger; 语句, 可以使用test.only, 只测试要debug的测试.  npm run test:debug开启测试. 在chrome 浏览器地址栏中 输入about:inspect

testEnvironme nt

使用Jest和React Test library 进行React单元测试的更多相关文章

  1. 如何使用TDD和React Testing Library构建健壮的React应用程序

    如何使用TDD和React Testing Library构建健壮的React应用程序 当我开始学习React时,我努力的一件事就是以一种既有用又直观的方式来测试我的web应用程序. 每次我想测试它时 ...

  2. react起步——从零开始编写react项目

    # index.html <!DOCTYPE html> <html> <head> <meta charset="utf-8" /> ...

  3. 【react学习】关于react框架使用的一些细节要点的思考

    ( _(:3 」∠)_给园友们提个建议,无论是API文档还是书籍,一定要多看几遍!特别是隔一段时间后,会有意想不到的收获的)   这篇文章主要是写关于学习react中的一些自己的思考:   1.set ...

  4. React Native 系列(二) -- React入门知识

    前言 本系列是基于React Native版本号0.44.3写的,最初学习React Native的时候,完全没有接触过React和JS,本文的目的是为了给那些JS和React小白提供一个快速入门,让 ...

  5. React-Native(三):React Native是基于React设计的

    React Native是基于React js设计的. 参考:<React 入门实例教程> React 起源于 Facebook 的内部项目,因为该公司对市场上所有 JavaScript ...

  6. 1. React介绍 React开发环境搭建 React第一个程序

    什么是 React         React 是 Facebook 发布的 JavaScript 库,以其高性能和独特的设计理念受到了广泛关注. React的开发背景         Faceboo ...

  7. [React] 02 - Intro: why react and its design pattern

    为啥使用React,给我个理由 过去 需要手动更新DOM.费力地记录每一个状态:既不具备扩展性,又很难加入新的功能,就算可以,也是有着冒着很大的风险. 不过,使用这种开发方式很难打造出极佳的用户体验. ...

  8. 转载 React.createClass 对决 extends React.Component

    先给出结论,这其实是殊途同归的两种方式.过去我们一般都会使用 React.createClass 方法来创建组件,但基于 ES6 的小小语法糖,我们还可以通过 extends React.Compon ...

  9. 【React 资料备份】React Hook

    Hooks是React16.8一个新增项,是我们可以不用创建class组件就能使用状态和其他React特性 准备工作 升级react.react-dom npm i react react-dom - ...

  10. [转] React 最佳实践——那些 React 没告诉你但很重要的事

    前言:对很多 react 新手来说,网上能找到的资源大都是些简单的 tutorial ,它们能教会你如何使用 react ,但并不会告诉你怎么在实际项目中优雅的组织和编写 react 代码.用谷歌搜中 ...

随机推荐

  1. ansible系列(21)--ansible的变量注册Register

    1. 变量注册Register register 关键字可以将某个 task 任务结果存储至变量中,最后使用 debug模块 输出变量内容,可以用于后续排障: 示例一:register的基本使用: # ...

  2. WEB服务与NGINX(6)-location使用详解

    目录 1. location的详细用法 1.1 精确匹配 1.2 区分大小写 1.3 不区分大小写 1.4 匹配URI开始 1.5 测试location的优先级 1.6 location的生产使用示例 ...

  3. python教程6.6-发送邮件smtplib

    实现步骤: Python对SMTP⽀持有 smtplib 和 email 两个模块, email 负责构造邮件, smtplib 负责发送邮件,它对smtp协议进⾏了简单的封装. 简单代码示例: 发送 ...

  4. cesium教程7-官方示例翻译-模型要素选择

    源代码示例: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UT ...

  5. C数据结构:KMP算法详解(呕心沥血)

    KMP算法 作者心声 了解暴力求解(必需会) KMP算法详解 记住我这段话(你会爱上它的)← : ①前后缀及其用处 ②求出前后缀的next数组 求出next数组的代码 开始实现KMP算法 结尾 附上源 ...

  6. JDK源码阅读-------自学笔记(二十二)(java.util.ArrayList自定义晋级,ArrayList实战详解)

    简介(Introduction)   上篇文章主要介绍了ArrayList自行模仿建立的方式,那么,其实这个类不是一次性就那么完美的,现在做一个一步步变成那样完整的ArrayList的版本升级测试. ...

  7. 前后端分离项目(vue+springboot)集成pageoffice实现在线编辑office文件

    前后端分离项目下使用PageOffice原理图 集成步骤 前端 vue 项目 在您Vue项目的根目录下index.html中引用后端项目根目录下pageoffice.js文件.例如: <scri ...

  8. 在 JS 中调整 canvas 里的文字间距

    实现说明: 在 JS 中 canvas 原生没有支持对文字间距的调整,我们可以通过将文字的每个字符单独渲染来实现.本案例从 CanvasRenderingContext2D 对象的原型链上扩展了一个用 ...

  9. d3d12龙书阅读----绘制几何体(上) 课后习题

    d3d12龙书阅读----绘制几何体(上) 课后习题 练习1 完成相应的顶点结构体的输入-布局对象 typedef struct D3D12_INPUT_ELEMENT_DESC { 一个特定字符串 ...

  10. PasteSpider之接口的授权实现为什么不采用JWT方式

    PasteTemplate序列的接口权限控制使用的都是一套逻辑 包括不限于PasteSpider,PasteTimer,PasteTicker等 大致逻辑一致,具体的细节可能会根据项目做一些调整! 实 ...