乐趣区

Jest + Enzyme 前端自动化测试

Jest、Enzyme 简介
Jest 是 Facebook 发布的一个开源的、基于 Jasmine 框架的 JavaScript 单元测试工具。
Enzyme 是 React 的测试类库。Enzyme 提供了一套简洁强大的 API,并通过 jQuery 风格的方式进行 DOM 处理,开发体验十分友好。
普通方法测试
首先,使用 npm 安装 Jest
npm install –save-dev jest
在目录下新建一个待测试文件 sort.js。
function sort(sortArr) {
return sortArr.sort((a, b) => a – b);
}
module.exports = sort;
此处 sort 方法未对入参做类型检测
在这里定义了一个数组排序方法,下面来书写其测试用例,在目录下新建一个 sort.test.js 文件。
const sort = require(‘./sort’);
const arr = [5,2,4,3,1];
test(‘ 排序数组[5,2,4,3,1]’, () => {
expect(sort(arr)).toEqual([1,2,3,4,5]);
})
在用例中,我们先引入了待测试的方法,接下来定义了一个排序数组 [5,2,4,3,1] 的测试用例.test()用来定义一个测试用例,expect()会执行内部的方法,返回一个待测试的结果。toEqual()用来判断返回的结果于期望的结果是否相等。这里由于期望返回结果为数组,所以使用 toEqual 进行判断,除此之外,还有 toBe(),toBeNull()等方法来比较不同的类型。更多内容 …
打开 package.json, 在 scripts 中新增
test: “jest”
然后运行命令
npm run test
会看到用例测试通过的信息

由于我们的方法没有做入参类型检测,下面通过传入字符串,来测试异常情况。在 sort.test.js 中新增一个测试用例用例
test(‘ 排序字符串“52431”’, () => {
expect(sort(‘52431’)).toEqual(12345);
})
运行,则会看到测试失败的信息

从测试结果中我们可以清除的看到,运行来两个测试用例,第一个用例通过来,第二个用例运行是 js 出现了报错。此时便能根据测试结果,调整代码
更多测试方法此处不做讨论,具体可以参考 Jest 文档
在具体项目中的使用
下面来在实际的项目中使用 Jest + Enzyme 来进行测试。测试 Demo 项目
首先,使用 Create-React-App 来创建一个应用。接着,安装 jest
npm install –save-dev jest
由于在书写用例时,会用到 es6 语法,所以还要安装 babel-jest 来进行转码
npm install –save-dev babel-jest
安装 enzyme
npm install –save-dev enzyme
也可以使用 react 官方测试插件 react-addons-test-utils,此处我们使用 enzyme,故不需要安装。
此外,还需要根据使用的 react 版本来安装 enzyme-adapter-react。具体版本对照如下

enzyme-adapter-react 版本
react 版本

enzyme-adapter-react-16
^16.4.0-0

enzyme-adapter-react-16.3
~16.3.0-0

enzyme-adapter-react-16.2
~16.2

enzyme-adapter-react-16.1
`~16.0.0-0 \
\
~16.1`

enzyme-adapter-react-15
^15.5.0

enzyme-adapter-react-15.4
15.0.0-0 – 15.4.x

enzyme-adapter-react-14
^0.14.0

enzyme-adapter-react-13
^0.13.0

此处 demo 使用的 react 版本为 ^16.4.1,所以我们需要安装 enzyme-adapter-react-16
npm install –save-dev enzyme-adapter-react-16
依赖安装完成,接下来需要进行相关的配置。
首先配置 package.json 的测试命令 test: “jest”。此时如果我们在根目录下创建一个.test.js 文件,并书写简单的方法用例,执行测试命令,是可以正常执行测试用例的。但是,我们的项目却并不是简单的单个方法但测试,实际项目中会存在这大量的组件依赖,还有 css,image 等静态资源的处理。所以,还要进行如下配置处理。
首先,我们在 package.json 文件中新增一个 jest 的配置项
jest: {}
这里我们主要进行三个配置。

moduleFileExtensions 代表支持加载的文件名。此处我们的测试文件均以.js 结尾,所以只配置成 [“js”] 即可

transform 用于编译 ES6/ES7 语法,需配合 babel-jest 使用

moduleNameMapper 代表需要被 Mock 的资源名称。如果需要 Mock 静态资源(如 less、scss 等),则需要配置 Mock 的路径

jest 默认会检索项目内的 *.test.js,*.test.jsx 形式的文件并执行。当编写当用例没被 jest 检索到时,可通过 moduleDirectories 来配置路径。
在具体到组件测试时,为了测试组件到交互性,我们需要 jest 渲染出组件进行操作,此时,由于我们到项目中大量使用来 webpack 到依赖管理,以及 less-loader、url-loader 等预编译。在 jest 渲染组件是,无法识别这些.less 等文件。所以我们需要通过 mock 来处理这些静态文件。因为 jest 在渲染组件时,是不需要依赖 css,image 等静态资源的。所以我们可以这样配置:
“\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$”: “<rootDir>/__mocks__/fileMock.js”,
“\\.(css|less)$”: “<rootDir>/__mocks__/styleMock.js”
前面通过正则来适配我们需要匹配的静态文件,后面为我们通过 mock 返回的数据。这里我们还需要在根目录中创建__mock__的文件夹。在里面新建 fileMock.js 和 styleMock.js 两个文件。
<!–fileMock.js–>
module.exports = ‘test-file-stub’;
<!–styleMock.js–>
module.exports = {};
这样就可以将测试集中在组件的结构和逻辑上。另外,可能在我们的项目中,会使用大量的别名来简化引用路径,及 webpack 中的 alias 配置。此处同样需要进行别名的配置,配置方式与静态资源配置类似。一下是完整配置
“jest”: {
“moduleFileExtensions”: [
“js”,
“jsx”
],
“moduleDirectories”: [
“src”,
“node_modules”
],
“transform”: {
“^.+\\.js$”: “babel-jest”
},
“moduleNameMapper”: {
“^components(.*)$”: “<rootDir>/src/components$1”,
“^pages(.*)$”: “<rootDir>/src/pages$1”,
“^utils(.*)$”: “<rootDir>/src/utils$1”,
“^services(.*)$”: “<rootDir>/src/services$1”,
“^static(.*)$”: “<rootDir>/src/static$1”,
“^models(.*)$”: “<rootDir>/src/models$1”,
“^variable(.*)$”: “<rootDir>//src/static/less/variable.less”,
“\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$”: “<rootDir>/__mocks__/fileMock.js”,
“\\.(css|less)$”: “<rootDir>/__mocks__/styleMock.js”
}
}
接下来创建一个待测试的组件,在 src > pages 文件夹中创建 login 组件,并配置好路由。组件代码参考测试 Demo 项目运行后页面如下
<img src=”https://github.com/ISummerRai…; width=”320″ />
接着,定义测试用例,此 Demo 定义来八个测试用例如下

1、页面 title 显示“登录”(UI)
2、登录账号输入手机号或邮箱时,账号上方显示登录账号
3、登录账号输入不为手机号或邮箱,账号上方显示【账户输入错误,请重新输入】
4、账号输入正常,密码小于 6 位,登录按钮置灰。
5、账号输入异常,密码不小于 6 位,登录按钮置灰。
6、账号输入正常,密码不小于 6 位,登录按钮可点。
7、点击密码后眼睛图标,显示密码。
8、显示密码状态,再次点击,隐藏密码。

接下来,新建文件 login.test.js 来编写测试用例代码。由于用例中设计多个交互,所以我们需要先渲染出组件。Enzyme 为我们提供来三种渲染组件的方法 shallow、render、mount。

shallow 方法就是官方的 shallow rendering 的封装。

render 方法将 React 组件渲染成静态的 HTML 字符串,然后分析这段 HTML 代码的结构,返回一个对象。它跟 shallow 方法非常像,主要的不同是采用了第三方 HTML 解析库 Cheerio,它返回的是一个 Cheerio 实例对象。

mount 方法用于将 React 组件加载为真实 DOM 节点。

三种方法中,shallow render 返回的为对象,用于分析 HTML 结构,所以无法用于交互测试。mount 方法加载的为真实的 DOM 节点,所以可用于交互测试。本 Login 组件存在大量交互测试,所以使用 mount 创建组件, 使用 mount 需要先使用 Adapter 配置如下
import Login from ‘pages/Login’;
import React from ‘react’;
import {configure} from ‘enzyme’;
import Adapter from ‘enzyme-adapter-react-16’;
import {mount} from ‘enzyme’;
configure({adapter: new Adapter() });

const wrapper = mount(<Login />);
现在,我们就可以使用 Enzyme 的 API 来编写测试用例了,Enzyme 提供了丰富的类 jquery 风格的 API,下面是部分 API
.get(index):返回指定位置的子组件的 DOM 节点
.at(index):返回指定位置的子组件
.first():返回第一个子组件
.last():返回最后一个子组件
.type():返回当前组件的类型
.text():返回当前组件的文本内容
.html():返回当前组件的 HTML 代码形式
.props():返回根组件的所有属性
.prop(key):返回根组件的指定属性
.state([key]):返回根组件的状态
.setState(nextState):设置根组件的状态
.setProps(nextProps):设置根组件的属性
完整 API 参见 Enzyme API
在前半部分的 demo 中,我们使用来 test() 方法来编写用例,此处,我们使用
describe(”, () => {
it(”, () => {})
})
来编写测试用例,这样我们可以对测试用例进行分组
让我们来开始第一个用例“页面 title 显示「登录」”的编写
it(‘ 标题显示 ’, () => {
const title = wrapper.find(‘.title’).text();
expect(title).toBe(‘ 登录 ’);
})
这个用例十分简单,仅仅在第一步获取到了 title 中的文本,并对文本进行校验。
第二个和第三个用例为对输入框输入文本对校验,此处,我们可以单独对校验方法进行测试,也可以页面对交互来完成测试。这里用例通过交互来进行测试用例对编写。由于在输入信息过程中,校验通过 input 框的 onChange 事件触发,所以我们需要用到 simulate 来触发事件。其中一个用例如下
const accountInput = wrapper.find(‘.account’).find(‘input’);
const accountTitle = wrapper.find(‘.account .name’).find(‘span’);
it(‘ 输入不合法账号 ’, () => {
const event = {
target: {
value: ‘abc123’
}
}
accountInput.simulate(‘change’, event);
expect(accountTitle.text()).toBe(‘ 账户输入错误,请重新输入 ’);
})
模拟输入来一个不合法的账号‘abc123’,验证失败,显示失败信息。
在 4,5,6 三个用例中,需要获取登录按钮 Button 组件的可点击状态,由于 enzyme 无法获取 css 状态,此时可以使用 API 中的 prop(key)来获取组件的 props 状态,从而判断组件的可点击状态。其中一个用例如下
it(‘ 输入正确账号,密码小于 6 位,指定状态 ’, () => {
wrapper.setState({
account: ‘18888888888’,
password: ‘12345’,
errorAccount: false
});
// 此处需重新获取 btn 对象,否则会导致用例失败
const submitBtn = wrapper.find(‘.btn-box’).find(‘Button’);
expect(submitBtn.props().disabled).toBe(true);
})
此处通过直接设置 state 的值来更改 Button 的状态。需要注意的是,为来减少重复定义,许多 Dom 对象的获取都在 describe 组下做了统一的定义,但在执行 expect 获取按钮状态是,需要重新查找,来获取最新但状态。除了直接指定 state 状态之外,还可以通过输入框输入,change 事件触发但方式来完成用例,如下
it(‘ 输入正确账号,密码小于 6 位,通过 change 触发 ’, () => {
const accountEvent = {
target: {
value: ‘18888888888’
}
};
const pwdEvent = {
target: {
value: ‘12345’
}
}
accountInput.simulate(‘change’, accountEvent);
passwordInput.simulate(‘change’, pwdEvent);
const submitBtn = wrapper.find(‘.btn-box’).find(‘Button’);
expect(submitBtn.prop(‘disabled’)).toBe(true);
7、8 两个用例使用但方法与上面相同,不再赘述。
所有用例编写完成之后,执行 npm run test 可以看到所有用例都通过测试。
测试覆盖率
在 package.json 文件的 test 命令修改为
test: “jest –coverage”
执行 npm run test 即可在用例执行信息后显示用例的覆盖率报告。

退出移动版