我们是袋鼠云数栈 UED 团队,致力于打造优秀的一站式数据中台产品。我们始终保持工匠精神,探索前端道路,为社区积累并传播经验价值。

前言

单元测试是一种用于测试“单元”的软件测试方法,其中“单元”的意思是指软件中各个独立的组件或模块。开发者需要为他们的代码编写测试用例以确保这些代码可以正常使用。

在我们的业务开发中,通常应用的是敏捷开发的模型。在此类模型中,单元测试在大部分情况下是为了确保代码的正常运行以及防止在未来迭代的过程中出现问题。

测试目的

1、排除故障

每个应用的开发中,多少会出现一些意料之外的 bug。通过测试应用程序,可以帮助我们大大减少此类问题,并且增强应用程序的逻辑性。

2、保证团队成员的逻辑统一

如果您是团队的新成员,并且对应用程序还不熟悉,那么一组测试就好像是有经验的开发人员监视你编写代码,确保您处于代码应该执行的正确路线之内。通过这些测试,您可以确信在添加新功能或更改现有代码时不会破坏任何东西。

3、可以提高质量代码

当您在编写 React 组件时,由于考虑到测试,最好的方案将是创建独立的、更可重用的组件。如果您开始为您的组件编写测试,并且您注意到这些组件不容易测试,那么您可能会重构您的组件,最终起到改进它们的效果。

4、起到很好的说明文档作用

测试的另一个作用是,它可以为您的开发团队生成良好的文档。当某人对代码库还不熟悉时,他们可以查看测试以获得指导,这可以提供关于组件应该如何工作的意图的洞察,并为可能要测试的边缘部分提供线索。

规范

工具

在袋鼠云数栈团队,我们建议使用 jest + @testing-library/react 来书写测试用例。后者是为 DOMUI 组件测试的软件工具。

基础语法

  • describe:一个将多个相关的测试组合在一起的块

  • test:将运行测试的方法,别名是it

  • expect:断言,判断一个值是否满足条件,你会使用到expect函数。 但你很少会单独调用expect函数, 因为你通常会结合expect和匹配器函数来断言某个值

  • skip:跳过指定的describe以及test,用法describe.skip/test.skip

  • cleanup:在每一个测试用例结束之后,确保所有的状态能回归到最初状态,比如在 UI 组件测试中,我们建议在 afterEach 中调用 cleanup 函数

    import { cleanup } from '@testing-library/react';
    
    describe('For test', () => {
    afterEach(cleanup);
    test('...', () => {})
    })

注意事项

1、函数命名

关于是使用 test 还是使用 it 的争论,我们不做限制。但是建议一个项目里,尽量保持风格一致,如果其余测试用例中均为 test,则建议保持统一。

2、业务代码

我们建议尽量把业务代码的函数的功能单一化,简单化。如果一个函数的功能包含了十几个功能数十个功能,那我们建议对该函数进行拆分,从而更加有利于测试的进行。

3、代码重构

在重构代码之前,请确保该模块的测试用例已经补全,否则重构代码的风险会过于巨大,从而导致无法控制开发成本。

4、覆盖率

我们建议尽量以覆盖率 100% 为目标。当然,在具体的开发过程中会有各种各样的情况,所以很少有能够达到 100% 的情况出现。

5、修复问题

每当我们修复了一个 bug,我们应当评估是否有必要为这个 bug 添加一个测试用例。如果需要的话,则在测试用例中新增一条以确保后续的开发中不会复现该 bug。

评估的参考内容如下:

  • 是否会造成白屏或其他严重的问题
  • 是否会影响用户的交互行为
  • 是否会影响内容的展示

以上内容,满足一条或多条,则认为应当为该 bug 新增测试用例。

6、toBe or toEqual

这两者的区别在于,toBe 是相等,即 ===,而 toEqual 是内容相同,即深度相等。我们建议基础类型用 toBe,复杂类型用 toEqual

我们需要测试什么

包括但不限于以下几种:

  • Component Data:组件静态数据
  • Component Props:组件动态数据
  • User Interaction:用户交互,例如单击
  • LifeCycle Methods:生命周期逻辑
  • Store:组件状态值
  • Route Params:路由参数
  • 输出的dom
  • 外部调用的函数
  • 对子组件的改变

单元测试场景

1、快照测试

如果是一个纯渲染的页面或者组件,我们可以通过快照记录最终效果,下一次快照结果会去对比是否正确。

使用场景:对于一个已知的固定的结果,我们使用快照去记录结果,每次进行测试会将最新结果和记录结果进行对比,如果一致,则代表测试通过,反之,则不然。

通常在测试 UI 组件时,我们会建议进行快照测试,以确保 UI 不会有意外的改变。这里我们建议使用 react-test-renderer 进行快照测试。

yarn add react-test-renderer @types/react-test-renderer -D

安装完成后,建议在 UI 测试的首个测试用例进行快照测试。

import React from 'react';
import renderer from 'react-test-renderer';
import { Toolbar } from '..'; test('Match Snapshot', () => {
const component = renderer.create(<Toolbar data={toolbarData} />);
const toolbar = component.toJSON();
expect(toolbar).toMatchSnapshot();
});

2、dom 结构测试

使用场景:对于当前组件接收到的参数或者数据,会对应渲染出一个固定结构,我们对结构进行解析,看是否与预期相符。比如表格的行数应该与接口返回的 list 长度一致,表格的表头应该固定是我们设定的文案,表格的对应某一格应该是接口返回的对应行和列的值。再比如组件内部根据接收的 props 的变量去判断显示 dom 结构,那我们在单测传入某一个值时,我们的预期应该是显示为什么样的。我们建议使用 @testing-library/jest-dom 做相关的测试

yarn add --dev @testing-library/jest-dom

测试例子如下:

import React from 'react';
import { render, waitFor } from '@testing-library/react';
import '@testing-library/jest-dom'; describe('Test Breadcrumb Component', () => {
test('Should support to render custom title', async () => {
const { container, getByTitle } = render(
<MyComponent
renderTitle={() => "I'm renderTitle";}
/>
); const testDom = await waitFor(() =>
container.querySelector('[title="test1"]')
);
const dom = await waitFor(() =>
container.querySelector('[title="I\'m renderTitle"]')
); expect(testDom).not.toBeInTheDocument();
expect(dom).toBeInTheDocument();
});
});

除了 toBeInTheDocument 外,还有其余接口,参见官方文档。

3、事件测试

使用场景:当组件或者页面上有点击事件,对于点击后发生的一系列动作是我们需要检测的,首先需要用 fireEvent 去模拟事件发生,然后测试事件是否正确触发,比如我的表单操作按钮,对于操作后的动作进行一一检测对应。

const btns = btnBox.getElementsByClassName('ant-btn');
// 取消
fireEvent.click(btns[0]);
await waitFor(() => {
expect(API.getProductListNew).toHaveBeenCalled();
});

4、function测试

function add(a, b){
return a+b;
}
it('test add function', () => {
expect(add(2,2)).toBe(4);
})

5、异步测试

使用场景:当你的预期需要时间等待

  • waitFor:可能会多次运行回调,直到达到超时
await waitFor(() => expect(mockAPI).toHaveBeenCalledTimes(1))
  • useFakeTimers:指定 Jest 使用假的全局日期、性能、时间和定时器 API,通常需要和runAllTicksrunAllTimers配合。
test('should warn if not saved custom type but clicked custom button', () => {
const { getByText, baseElement } = wrapper; jest.useFakeTimers();
fireEvent.click(getByText('自定义类型'));
fireEvent.mouseDown(getByText('自定义类型')); expect(getByText('名称不能为空')).toBeInTheDocument();
jest.runAllTimers(); const inputEle = baseElement.querySelector('.dt-input');
fireEvent.change(inputEle, { target: { value: '1' } });
jest.useFakeTimers();
fireEvent.click(getByText('自定义类型')); expect(getByText('请先保存')).toBeInTheDocument();
jest.runAllTimers();
});

6、模拟属性和方法的返回结果

使用场景:当访问的某些属性或者方法在当前环境不存在时。

// 已有属性:jest.spyOn,例子如下
jest.spyOn(document.documentElement, 'scrollWidth', 'get').mockImplementation(() => 100); // 未知属性:Object.defineProperty,例子如下
Object.defineProperty(window, 'getComputedStyle', { value: jest.fn(() => ({ paddingLeft: '0px'}) // 方法的返回结果:jest.mock
function = jest.mock(() => {})

7、Drag

有时候,我们需要去测试拖拽功能,我们建议用以下函数来执行模拟拖拽的操作

import { fireEvent } from '@testing-library/react';

function dragToTargetNode(source: HTMLElement, target: HTMLElement) {
fireEvent.dragStart(source);
fireEvent.dragOver(target);
fireEvent.drop(target);
fireEvent.dragEnd(source);
}

8、test.only

在出现测试用例无法通过,但是又判断代码的逻辑没有问题之后,将该条测试用例设置为 only 再跑一遍测试用例,以确保不是其他测试用例导致的该测试用例的失败。这类问题经常出现自代码中欠缺深拷贝,导致多条测试用例之中修改了原数据从而使得数据不匹配。

例如:

// mycode.ts
function add(record: Record<string, any>){
Object.assign(record, { flag: false});
} // mycode.test.ts
const mockData = {};
test('',() => {
add(mockData)
...
...
}) test.only('',() => {
add(mockData) // the mockData is modified by add function here
...
...
})

在项目中遇到的一些问题

1、执行 pnpm test 报错

原因:当引入外部库是es模块时, jest无法处理导致报错,可以通过 babel-jest 进行处理,根据官方文档:https://jestjs.io/zh-Hans/docs/26.x/getting-started,还有一种就是修改jest.config.js 加入preset: 'ts-jest' ,会让部分测试成功但是还是会存在一些问题。

方案一:采用了 babel-jest 进行处理

pnpm add -D babel-jest @babel/core @babel/preset-env

安装完以后在工程的根目录下创建一个babel.config.js

module.exports = {
presets: [['@babel/preset-env', {targets: {node: 'current'}}]],
};

修改jest.config.js,增加transform

transform: {
"^.+\\.js$": "babel-jest",
"^.+\\.(ts|tsx)$": "ts-jest",
},

方案二:仍然采用 ts-jest ,把引起报错文件的后缀,如 js 改为 ts 即可

2、ts-jest和jest版本未对应

报如下错误

升级后版本(仅供参考)

3、toBeInTheDocument、toHaveClass等报错

类型检查错误,应该是@testing-library/jest-dom类型没被引入导致的

有以下两种方案,都需要修改tsconfig.json

// 方案一,删除typeRoots
"typeRoots": ["node", "node_modules/@types", "./typings"] // 方案二,添加types
"types": ["@testing-library/jest-dom"]

参考链接:https://stackoverflow.com/questions/57861187/property-tobeinthedocument-does-not-exist-on-type-matchersany

4、Cannot find namespace 'NodeJS’

修改 tsconfig.json ,往 types 中加入 node

"types": ["node", "@testing-library/jest-dom"]

5、module 'tslib' cannot be found

报错信息如下

原因是在 tsconfig.json 中开启了如下配置

"importHelpers": true,

编译文件会引入tslib可以参考

https://juejin.cn/post/6953554051879403534

https://github.com/microsoft/TypeScript/issues/37991

解决方案如下:

方案一:

"importHelpers": false,

方案二:

pnpm add tslib

并且修改 tsconfig

"paths": {
"tslib" : ["./node_modules/tslib/tslib.d.ts"] //在paths下添加tslib路径
}

6、由于单测的运行环境问题,当遇到某些方法没有的时候尝试mock下

例如:

解决方案如下:

(global as any).document.createRange = () => ({
selectNodeContents: jest.fn(),
getBoundingClientRect: jest.fn(() => ({
width: 500,
})),
});

7、多个单测文件缺失某一个方法,可以采用如下配置

例如:多个单测文件有如下报错:

那么首先在 jest.comfig.js 中添加配置

module.exports = {
setupFilesAfterEnv: ['./setupTests.ts'],
// ...
}

然后在 setupTests.ts 文件中:

Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation((query) => ({
matches: false,
media: query,
onchange: null,
addListener: jest.fn(), // deprecated
removeListener: jest.fn(), // deprecated
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
})),
});

8、The error below may be caused by using the wrong test environment;Consider using the "jsdom" test environment

依赖版本:

"ts-jest": "^28.0.8",
"jest": "^28.1.2",

解决方法: 在 jest.config.js 中添加配置

module.exports = {
verbose: true,
testEnvironment: 'jsdom',
// ...
}

并安装 jest-environment-jsdom (注意: 仅 jest 28 及更高版本需要安装此依赖项)

{
"devDependencies": {
"jest-environment-jsdom": "^28.1.2",
}
}

9、Echarts 单元测试 canvas 报错

在写 Echarts 单元测试的时候,会有 canvas 报错。原因很明显,Echarts 依赖了 canvas。

解决办法:使用 jest-canvas-mock,参考:Error: Not implemented: HTMLCanvasElement.prototype.getContext

注意:直接引入 canvas 虽然可以解决单元测试的报错,但是会导致安装依赖会有偶发性 canvas 报错。

10、引入了第三方的组件CodeMirrorEditor写单测报错

在对该组件进行单测时,由于引入了第三方的组件 CodeMirrorEditor ,编译时出现了以下问题,原因是试图导入 jest 无法解析的文件,而从实际上来说我们对当前组件的测试其实并不用去编译 dt-react-codemirror-editor。

因此,在 jest.config.js 文件加入编译时需要忽略的文件。

再次运行测试,然而。。。。。。

好吧,又失败了进入 index 查看,提示找不到 style 文件但是文件夹里又是存在的,初步尝试是否由于文件扩展名起,保存测试通过,但是修改 node_modules 里的文件扩展名无法从根本解决该问题,按照推荐提示在测试覆盖文件扩展名 moduleFileExtensions 内加入 css。

再次尝试,然而。。。。。。jest 去编译了 style.css 文件,然后它无法解析失败了,查看配置。

发现已经配置了当匹配到 css 文件时映射到一个空对象里,并不会去编译原样式文件,原因是由于加入到了编译覆盖的文件扩展名数组里 moduleFileExtensions,因此无法采用推荐方法。

再次回顾问题产生的原因,jest 无法找到 style 文件但是找到了 style.css 文件,但是 style 文件我们并不需要进行编译,加入 moduleNameMapper 当找到 style 文件时映射到一个空对象的文件里。

11、Route && Link

在测试面包屑组件BreadCrumb时,因为面包屑组件中只用了 Link 标签,最终会被转成 a 标签,用来路由导航。如下写法是将 Link 和 route 放在一个组件之中。然后报错:Invariant Violation: <Link>s rendered outside of a router context cannot navigate

import React from 'react'
import BreadCrumb from '../index';
import { render, fireEvent } from '@testing-library/react'
import '@testing-library/jest-dom/extend-expect';
import { Router, Switch, Route } from 'react-router-dom';
import { createMemoryHistory } from 'history'
const testProps = {
breadcrumbNameMap: [
{
name: 'home',
path: '/home'
},
{
name: 'home/about',
path: '/home/about'
}
],
style: {
backgroundColor: '#dedede'
}
}
const Home = () => <h1>home</h1>
const About = () => <h1>about</h1>
const App = () => {
const history = createMemoryHistory();
return (
<>
<Router history={history}>
{< BreadCrumb {...testProps} />}
<Switch>
<Route exact path="/main" component={Home} />
<Route path="/main/home" component={About} />
</Switch>
</Router>
</>
)
}
describe('test breadcrumb', () => {
test('should navigate to home when click ', () => {
const { container, getByTestId } = render(<App />);
expect(container.innerHTML).toMatch('about')
fireEvent.click(getByTestId('/home-link'))
expect(container.innerHTML).toMatch('home')
})
})

主要原因是版本原因:3.0版本路由不支持这种写法。3.0是将react-routerreact-router-dom分开的;而4.0路由将其合并成了一个包,在具体使用时应该基于不同的平台要使用不同的绑定库。例如在浏览器中使用 react router,就安装 react-router-dom 库;在 React Native 中使用 React router 就应该安装 react-router-native 库,但是我们不会安装 react-router了。项目中用的是3.0版本路由,于是改为3.0写法,将linkrouter分开写在两个组件中,通过测试

const testProps = {
breadcrumbNameMap: [
{
name: 'home',
path: '/home'
},
{
name: 'about',
path: '/about'
}
],
style: {
backgroundColor: '#dedede'
}
}
const App = (props) => {
return (
<div>
{<BreadCrumb {...testProps} />}
{props.children}
</div>
)
}
const About = () => <h1>about page</h1>
const Home = () => <h1>home</h1> describe('test breadcrumb', () => {
afterEach(() => {
cleanup();
})
test('should navigate to home router when click ', () => {
const history = createMemoryHistory()
const { container, getByTestId } = render(
<Router history={history}>
<Route path="/" component={App}>
<IndexRoute component={About} />
<Route path="/about" component={About} />
<Route path="/home" component={Home} />
</Route>
</Router>
);
expect(container.innerHTML).toMatch('about')
fireEvent.click(getByTestId('/home-link'))
expect(container.innerHTML).toMatch('home')
})
})

参考文献

Jest + React 单元测试最佳实践的更多相关文章

  1. 【转】.NET(C#):浅谈程序集清单资源和RESX资源 关于单元测试的思考--Asp.Net Core单元测试最佳实践 封装自己的dapper lambda扩展-设计篇 编写自己的dapper lambda扩展-使用篇 正确理解CAP定理 Quartz.NET的使用(附源码) 整理自己的.net工具库 GC的前世与今生 Visual Studio Package 插件开发之自动生

    [转].NET(C#):浅谈程序集清单资源和RESX资源   目录 程序集清单资源 RESX资源文件 使用ResourceReader和ResourceSet解析二进制资源文件 使用ResourceM ...

  2. ABAP单元测试最佳实践

    本文包含了我在开发项目中经历过的实用的ABAP单元测试指导方针.我把它们安排成为问答的风格,欢迎任何人添加更多的Q&A's,以完成这个列表. 在我的项目中,只使用传统的ABAP report. ...

  3. 关于单元测试的思考--Asp.Net Core单元测试最佳实践

    在我们码字过程中,单元测试是必不可少的.但在从业过程中,很多开发者却对单元测试望而却步.有些时候并不是不想写,而是常常会碰到下面这些问题,让开发者放下了码字的脚步: 这个类初始数据太麻烦,你看:new ...

  4. Junit内部解密之四: Junit单元测试最佳实践

    我们做使用Junit工具来做单页测试或接口测试时,需要注意一些问题,包括我们的编码规范,test规范,以及编写测试代码的策略,以下个人的总结. 1.为还没有实现的测试代码抛出一个异常.这就避免了该测试 ...

  5. 腾讯优测优分享 | 探索react native首屏渲染最佳实践

    腾讯优测是专业的移动云测试平台,旗下的优分享不定时提供大量移动研发及测试相关的干货~ 此文主要与以下内容相关,希望对大家有帮助. react native给了我们使用javascript开发原生app ...

  6. 探索react native首屏渲染最佳实践

    文 / 腾讯 龚麒 0.前言 react native给了我们使用javascript开发原生app的能力,在使用react native完成兴趣部落安卓端发现tab改造后,我们开始对由react n ...

  7. 总结 React 组件的三种写法 及最佳实践 [涨经验]

    React 专注于 view 层,组件化则是 React 的基础,也是其核心理念之一,一个完整的应用将由一个个独立的组件拼装而成. 截至目前 React 已经更新到 v15.4.2,由于 ES6 的普 ...

  8. React服务器渲染最佳实践

    源码地址:https://github.com/skyFi/dva-starter React服务器渲染最佳实践 dva-starter 完美使用 dva react react-router,最好用 ...

  9. 我们编写 React 组件的最佳实践

    刚接触 React 的时候,在一个又一个的教程上面看到很多种编写组件的方法,尽管那时候 React 框架已经相当成熟,但是并没有一个固定的规则去规范我们去写代码. 在过去的一年里,我们在不断的完善我们 ...

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

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

随机推荐

  1. 关于Intent.setDataAndType参数问题

    关于Intent.setDataAndType参数问题 install取设置属于和类型,数据就是获取到的uri,更具文件类型不同,type参数也不相同,具体参考下表 {后缀名,MIME类型} ​ {& ...

  2. Opengl ES之YUV数据渲染

    YUV回顾 记得在音视频基础知识介绍中,笔者专门介绍过YUV的相关知识,可以参考: <音视频基础知识-YUV图像> YUV数据量相比RGB较小,因此YUV适用于传输,但是YUV图不能直接用 ...

  3. jquery 简单分页插件jQuerypage

    昨天项目手机端要用到table的分页,考虑到手机端界面小,系统数据不多,在没考虑大批量数据处理的前提前就下载了这个插件,简单. 展示数据datas为json格式. <!DOCTYPE html& ...

  4. 【Java SE】Day04 IDEA、方法*

    一.IDEA 1.快捷键 Ctrl+Alt+L/Ctrl+Alt+Shift+4:格式化代码 Alt+Insert:自动生成代码 修改快捷键:File->Settings->keymap- ...

  5. 【每日一题】【栈和队列、双端队列】20. 有效的括号/NC52 有效括号序列-211127/220126

    给定一个只包括 '(',')','{','}','[',']' 的字符串 s ,判断字符串是否有效. 有效字符串需满足: 左括号必须用相同类型的右括号闭合.左括号必须以正确的顺序闭合. 来源:力扣(L ...

  6. 最新 2022 年 Kubernetes 面试题高级面试题及附答案解析

    题1:Kubernetes Service 都有哪些类型? 通过创建Service,可以为一组具有相同功能的容器应用提供一个统一的入口地址,并且将请求负载分发到后端的各个容器应用上.其主要类型有: C ...

  7. MongoDB数据库与Python的交互

    一.缘由 这是之前学习的时候写下的基础代码,包含着MongDB数据库和Python交互的基本操作. 二.代码实现 import pymongo #连接数据库 client=pymongo.MongoC ...

  8. jupyter 数据显示设置

    #设置显示行数pd.set_option('display.max_row',None)#设置显示列数pd.set_option('display.max_column',None)#设置显示宽度pd ...

  9. [OpenCV实战]3 透明斗篷

    目录 1寻找和存储背景帧 2红色区域检测 3提取红色区域 4背景帧红布区域替换当前帧红布区域. 5工程代码 参考 弄出哈利波特电影里一样效果的透明斗篷.也就是一个视频里,将红布弄成透明.类似下面的效果 ...

  10. VSCode运行C/C++配置

    将MinGw安装目录下的 1.安装 VSCode 2.安装 MinGW 链接:点击跳转 3.MinGW 内安装两个模块 1.右键 Mark for Installation 勾选 (此处已安装好,所以 ...