Jest学习笔记,测试环境

Web 前端单元测试到底要怎么写?看这一篇就够了

2018/08/16 · 基础技术 ·
单元测试

原稿出处: deepfunc   

随着 Web
应用的复杂程度越来越高,很多小卖部更是爱护前者单元测试。大家来看的抢先五分三科目都会讲单元测试的根本、一些有代表性的测试框架
api
怎么使用,但在实质上项目中单元测试要怎么入手?测试用例应该包括如何具体内容呢?

正文从贰个真实的施用场景出发,从设计格局、代码结构来分析单元测试应该包罗哪些内容,具体育项目测验试用例怎么写,希望观望的童鞋都能具有收获。

壹 、Jest 基本概念

       Jest
是Facebook的二个专门开始展览Javascript单元测试的工具,适合React全家桶使用,具有零配置、内置代码覆盖率、强大的Mocks等特点。

安装:

  1. npm i jest -D(安装到地面)
  2. npm i jest -g(安装到全局)

运行:

  1. npx jest(安装到地点的前提下)
  2. jest(安装到全局的前提下)
  3. 修改"script" 中的"test""jest"后使用npm run testnpm t

零配置:
但测试用例文件名格式为**test.js(暗中同意配置下)

主导方法:

  • 分组(Test Group):descripe(描述语,function)
  • 测试用例(Test Case):test(描述语,function)
  • 断言(Assert):expect(运行需测试的方法并返回实际结果).toBe(预期结果)

例:

Pencil.query =(name, url)=> { //方法,再次回到抓获
    // ?hello=test&wonghan=handsome
    var reg = new RegExp(‘(?:\?|&)’ + name + ‘=(.*澳门葡京,?)(?:&|$)’)
    var ret = reg.exec(url) || []
    return ret[1]
}

test(‘query’,()=>{ // testCase
    // 断言
    expect(Pencil.query(‘hello’, ‘?hello=test’)).toBe(‘test’)
    expect(Pencil.query(‘hello’, ‘?hello2=test’)).toBe(undefined)
    //能够写几个ecpect()
})

test(‘query2’,()=>{
    expect(Pencil.query(‘hello/test’,
‘?hello/test=test’)).toBe(‘test’)
})
    //能够写多少个test()

describe(‘test query’,()=>{
test(‘query3’,()=>{ // testCase
    // assert
    expect(Pencil.query(‘hello’, ‘?hello=test’)).toBe(‘test’)
    expect(Pencil.query(‘hello’, ‘?hello2=test’)).toBe(undefined)
    })
})

结果:

澳门葡京 1

测试结果

您也足以应用Jest的“匹配器”来测试数据。有好多不一的匹配器,所以那篇文章只会介绍最常用的三种。

1.为什么要动用单元测试工具?

因为代码之间的相互调用关系,又希望测试进度单元相互独立,又能健康运作,那就必要大家对被测函数的注重函数和环境举行mock,在测试数据输入、测试执行和测试结果检查方面存在很多相似性,测试工具正是为我们在这一个地点提供了福利。

所谓单元测试也正是对各样单元实行测试,通俗的将一般针对的是函数,类或单个组件,不涉及系统和集成。单元测试是软件测试的功底测试。

品类用到的技术框架

该类型采纳 react
技术栈,用到的要紧框架包蕴:reactreduxreact-reduxredux-actionsreselectredux-sagaseamless-immutableantd

二、Jest 配置

即便说Jest是零陈设,但也是足以计划

平凡匹配

2.React 的标配测试工具 Jest。

Jest首要有以下特征:

      1.适应性:Jest是模块化、可扩张和可布置的。

    
2.沙箱和飞跃:Jest虚拟化了JavaScript的条件,能模仿浏览器,并且并行执行

     3.快速照相测试:Jest能够对React
树举行快速照相或别的种类化数值快速编写测试,提供高效更新的用户体验。

     4.支撑异步代码测试:援救promises和async/await

    
5.自动生成静态分析结果:不仅展现测试用例执行结果,也展现语句、分支、函数等覆盖率。

JEST相比较Mocha来说,因为正如多少个优点最终胜出:

     1.和React师出同门,FB官方扶助

     2.曾经集成了测试覆盖率检查、mock等作用,不必要安装额外的库

    
3.文书档案完备,官方提供了和babel、webpack集成情形下以及异步调用的测试消除方案

     4.官方提供snapshot testing消除方案

动用场景介绍

澳门葡京 2

其一利用场景从 UI 层来讲至关主要由四个部分构成:

  • 工具栏,包蕴刷新按钮、关键字搜索框
  • 报表彰显,采纳分页的款式浏览

观察那里有的童鞋大概会说:切!这么不难的界面和业务逻辑,依旧实打实情况呢,还需求写神马单元测试吗?

别急,为了保险小说的阅读经验和长短适中,能讲精通难题的简练场景就是好现象不是吗?慢慢往下看。

(一)配置地点

1. package.json
package.json累加配备项"jest" : { 配置项 }

2. jest.config.js
新建jest.config.js并丰盛配置项module.exports = { 配置项 }

3. 命令行(独有的option)
见:命令行配置

最简单易行的的测试方法正是看多少个相比较值是或不是等于:

3.Jest + Enzyme的行使进程

1.安装

$ nam install —save-dev jest

一旦必要在测试项目中应用babel,必要设置babel-jest

$ nam install —save-dev babel-jest

接下来安装enzyme

$ npm install enzyme —save-dev

借使应用的react版本在13之上,还亟需安装react-addons-test-utils

$ nam i —save-dev react-addons-test-utils

 

2.配置

JEST运转基础功用尽管无需安插,不过官方依旧提供了配备选项来落到实处本性化供给。

package.json文件中配置jest的collectCoverageFrom参数,来钦赐检查有着须求测试的公文(无论源文件有没有被测试文件使用到)

coverageThreshold 参数来配置测试覆盖率。

 

jest.config.js 配置jest:

module.exports = {

  bail: true, //遇上 test feature, 则Stop running test, 默许值是false

  cacheDirectory: ‘./node_modules/.cache’, //测试缓存数据的贮存地方

  testEnvironment: ‘jsdom’, //default brower-like enviroment,
假设你搭建了三个node service node-like enviroment

  coverageThreshold: { //测试覆盖率, 阈值不知足,就回去测试战败

    global: {

Jest学习笔记,测试环境。      branches: 90,

      functions: 90,

      lines: 90,

      statements: 90,

    },

  },

  coveragePathIgnorePatterns: [ //该路径下的测试,忽略在测试覆盖率上

    ‘build’,

    ‘<rootDir>/src/shared/libs/url/’,

  ],

  testRegex: ‘test/.*\\.jsx?$’, //要测试的文件目录及后缀

  testPathIgnorePatterns: [ //忽略该路线的文件测试

    ‘<rootDir>/node_modules/’,

    ‘<rootDir>/build/’,

    ‘<rootDir>/scripts/’,

    ‘<rootDir>/api/’,

    ‘<rootDir>/test/setup.js’,

    ‘__mocks__’,

  ],

  moduleFileExtensions: [”, ‘json’, ‘js’, ‘jsx’, ‘less’],
//测试模块中用到的文本的后缀名配置

  modulePaths: [‘<rootDir>/src’, ‘<rootDir>’],

  moduleNameMapper: {  //与测试无关的财富文件同意mock 掉,那样在import
的时候就不会真的引入那些文件

    ‘^import?’: ‘<rootDir>/build/jestImportMock.js’,

    ‘\\.(css|less|gif|jpg|jpeg|png)$’:
‘<rootDir>/build/jestStyleMock.js’,

  },

  setupFiles: [‘<rootDir>/test/setup.js’],
//给种种测试文件添加额外的安排

  transformIgnorePatterns: [ //测试进程不改变满意配置的文书

    ‘<rootDir>/node_modules/(?!(react-aaui|tempest\\.js)/)’,

    ‘babel-runtime’,

  ],

}

 

设计形式与组织解析

在那个处境设计开发中,大家严厉服从 redux 单向数据流 与 react-redux
的一流实践,并应用 redux-saga 来处理业务流,reselect
来处理意况缓存,通过 fetch 来调用后台接口,与诚实的项目并未差距。

分段设计与代码组织如下所示:
澳门葡京 3

中间 store 中的内容都是 redux 相关的,看名称应当都能了然意思了。

实际的代码请看 这里。

(二)配置项

1. testMatch
安装识别哪些文件是测试文件(glob方式),与testRegex互斥,不可能同时写

testMatch:
[‘**/__tests__/**/*.js?(x)’,’**/?(*.)(spec|test).js?(x)’]

2. testRegex
设置识别哪些文件是测试文件(正则格局),与testMatch互斥,不能够而且写

testRegex: ‘(/__tests__).*|(\\.|/)(test|spec))\\.jsx?$’

3. testRnviroment
测试环境,私下认可值是:jsdom,可修改为node

testEnvironment: ‘jsdom’

4. rootDir
默许值:当前目录,一般是package.json所在的目录。

rootDir: ‘ ‘

5. moduleFileExtensions
测试文件的项目

moduleFileExtensions: [‘js’,’json’,’jsx’,’node’]

诚如配备:

module.exports = {
    testMatch: [‘<rootDir>/test/**/*.js’],
    testEnvironment: ‘jsdom’,
    rootDir: ”,
    moduleFileExtensions: [‘js’,’json’,’jsx’,’node’]
}

test(‘2加2等于’,()=>{
    expect(2+2).toBe(4);
});

4.驾驭React官方测试工具库

react测试能够分成测试DOM结构 和测试Action和Reducer

React官方测试工具库提供两种测试方式:

1.Shallow Rendering 测试虚拟DOM的法门 

Shallow Rendering
(浅渲染)指的是,将多个零部件渲染成虚拟DOM对象,可是只渲染第②层,不渲染全部子组件,所以处理速度非常的慢。它不须要DOM环境,因为根本没有加载进DOM。

import TestUtils from ‘react-addons-test-utils’;

function shallowRender(Component) {

  const renderer = TestUtils.createRenderer();

  renderer.render(<Component/>);

  return renderer.getRenderOutput();

}

Shallow Rendering
函数,该函数再次来到的正是四个浅渲染的虚构DOM对象。唯有一层,不重回子组件。

2.DOM Rendering 测试真实DOM的艺术

官方测试工具库的第叁种测试方法,是将零件渲染成真正的DOM节点,再拓展测试。那时就供给调用renderIntoDocument
方法。

import TestUtils from ‘react-addons-test-utils’;

import App from ‘../app/components/App’;

const app = TestUtils.renderIntoDocument(<App/>);

renderIntoDocument
方法必要存在3个实际的DOM环境,不然会报错。由此,测试用例之中,DOM环境(即window,
document 和 navigator 对象)必须是存在的

 

Enzyme库对官方测试库举行了打包,它提供二种艺术:

import { shallow, mount, render } from ‘enzyme’

shallow 重临组件的浅渲染,对官方shallow rendering 举行打包

const wrapper = shallow(<Counter {…props} />)

expect(wrapper.find(‘button’).exists()).toBeTruthy()

shallow 再次来到Counter 的浅渲染,然后调用find 方法寻找 button 成分

至于find方法,有二个注意点,正是它只辅助不难选用器,稍微复杂的少数的CSS选取器都不支持。

render
方法将React组件渲染成静态的HTML字符串,然后分析那段HTML代码的组织,再次来到四个对象。它跟shallow方法充足像,主要的差别是选择了第二方HTML解析库Cheerio,它回到的是2个Cheerio实例对象。

const wrapper = render(<Counter {…props} />)

expect(wrapper.find(‘button’).exists()).toBeTruthy()

render方法与shallow方法的API基本是一律的。

Enzyme的安顿性就是,让差异的平底处理引擎,都存有同样的API

 

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

const wrapper = mount(<Counter arithmetic={arithmetic})

 wrapper.find(‘button’).simulate(‘click’)

 

Enzyme的一局地API,你能够从中理解它的光景用法。

.get(index):再次来到钦命地方的子组件的DOM节点

.at(index):再次来到钦点地方的子组件

.first():再次来到第壹身材组件

.last():重回最终叁个子零部件

.type():重临当前组件的档次

.text():重返当前组件的文件内容

.html():重返当前组件的HTML代码格局

.props():重回根组件的有着属性

.prop(key):再次来到根组件的钦定属性

.state([key]):再次回到根组件的地方

.setState(nextState):设置根组件的景观

.setProps(nextProps):设置根组件的属性

 

toMatchSnapshot方法会去帮您相比本次即将变化的构造与上次的分别

 

测试 异步action

她的I/O或许凭借store.getState(),自己又会凭借异步中间件,那类使用原生js测试起来相比较困难,大家的指标可以设定为:当大家接触四个action后,它经历了三个圈异步最后store.getAction中的action拿到的数量和咱们预料一致。由此我们要求用到三个库:redux-mock-store
和 nock。

const mockStore = configureStore([thunk, promiseMiddleware])
//配置mock 的store,让她们有雷同的middleware

afterEach(() => 

 nock.cleanAll()

)  //每执行完3个测试后,清空nock

 

const store = mockStore({

   router: {

      location: ‘/’,

    },

})  //以大家约定的初步state创设store,控制 I/O 正视

 

const data = [

//接口重回的消息

{…

},

]

 

nock(API_HOST) //拦截请求重返的response

.get(`/api/…`) //拼接路由,须求在test.js中布署测试路径

.reply(200, {code: 0, data})

return store.dispatch(actions.getAll()).then(() =>
expect(store.getActions()).toMatchSnapShot())

 

1.用nock来mock拦截http请求结果,并赶回大家加以的response

2.用redux-mock-store 来mock store
的生命周期,要求事先把middleware配成和类型同样

3.describe会包蕴部分生命周期的api,比如整个测试初叶做吗,单个测试甘休做什么api,那里每执行完二个测试就清空nock

4.用了jest中的toMatchSnapShot api 来判定八个原则是不是相同。

理所当然你要写成 expect(store.getActions()).toEqual({data …}) 你需求把equal
里的事物都描写具体,而toMatchSnapshot

可在当前目录下生成二个snapshot
,专门存放当前结果,写测试时看一眼是意料的就commit。假诺改坏了,函数就不匹配snapshot了。

单元测试部分介绍

先讲一下用到了哪些测试框架和工具,主要内容包涵:

  • jest ,测试框架
  • enzyme ,专测 react ui 层
  • sinon ,具有独自的 fakes、spies、stubs、mocks 功效库
  • nock ,模拟 HTTP Server

即使有童鞋对下面那么些使用和配备不熟的话,直接看官方文书档案吧,比任何学科都写的好。

接下去,大家就开首编写制定具体的测试用例代码了,上面会针对种种层面给出代码片段和剖析。那么咱们先从
actions 开始吧。

为使小说尽量简单、清晰,下边包车型大巴代码片段不是每种文件的欧洲经济共同体内容,完整内容在
这里 。

三、Jest Matchers

       Matchers俗称断言库,例如地点的expect().toBe()便是中间之一,其余大规模用法如下:

在那段代码中,expect(2 + 2)
重返2个“expectation”对象。针对expectation对象,你除了调用匹配器,其余的你怎么着也做不了。在那段代码中,.toBe(4)就是匹配器。当Jest运转时,它会跟踪全体的匹配器,假设有挫折的合作就会打字与印刷出荒谬消息。

5.测试注意事项

1.拆分单元,关切输入输出,忽略中间经过。dom测试时只用确定保障正确调用了action函数,传参正确,而不用关爱函数调用结果,置于action处理结果,reducer中对state的变动这几个都留下action和reducer自身的单元测试区测。不要想着测试整个大意义的流程,不要有闭环的构思,单元测试需求保障的脚下单元春常,对于每一种单元模块输入输出都没错,理论串联后联手使用闭环时也会正确。

 

2.开外场所的测试覆盖,如若不可能保证测试的周密性,每个境况都覆盖到,那么那一个测试就是个不敢依靠的不圆满的测试。当然在骨子里项目中,或许因为日子、财富等题材,不恐怕保险每一个景况都测试到,而只测试主要的始末,那时候要到位心里有数,反正自个儿是对于每一种测试都写注释的,交代清楚测试覆盖了哪些,还有怎么样没有掩盖,要求任何手段有限支撑安澜。

 

3.关怀该关切的,无关首要的mock掉。css、图片那种mock掉,http请求mock掉

 

4.原本不便于测试的代码依旧需求修改的,并无法为了原代码稳定不变,在测试时不敢动原代码。譬如函数不纯,没有重返值等。

 

actions

事务里面作者使用了 redux-actions 来产生
action,这里用工具栏做示范,先看一段工作代码:

import { createAction } from ‘redux-actions’; import * as type from
‘../types/bizToolbar’; export const updateKeywords =
createAction(type.BIZ_TOOLBAR_KEYWORDS_UPDATE); // …

1
2
3
4
5
6
import { createAction } from ‘redux-actions’;
import * as type from ‘../types/bizToolbar’;
 
export const updateKeywords = createAction(type.BIZ_TOOLBAR_KEYWORDS_UPDATE);
 
// …

对于 actions 测试,我们任重(英文名:rèn zhòng)而道远是印证发生的 action 对象是或不是正确:

import * as type from ‘@/store/types/bizToolbar’; import * as actions
from ‘@/store/actions/bizToolbar’; /* 测试 bizToolbar 相关 actions */
describe(‘bizToolbar actions’, () => { /* 测试革新摸索关键字 */
test(‘should create an action for update keywords’, () => { //
营造指标 action const keywords = ‘some keywords’; const expectedAction =
{ type: type.BIZ_TOOLBAR_KEYWORDS_UPDATE, payload: keywords }; //
断言 redux-actions 产生的 action 是或不是科学
expect(actions.updateKeywords(keywords)).toEqual(expectedAction); }); //
… });

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import * as type from ‘@/store/types/bizToolbar’;
import * as actions from ‘@/store/actions/bizToolbar’;
 
/* 测试 bizToolbar 相关 actions */
describe(‘bizToolbar actions’, () => {
  
    /* 测试更新搜索关键字 */
    test(‘should create an action for update keywords’, () => {
        // 构建目标 action
        const keywords = ‘some keywords’;
        const expectedAction = {
            type: type.BIZ_TOOLBAR_KEYWORDS_UPDATE,
            payload: keywords
        };
 
        // 断言 redux-actions 产生的 action 是否正确
        expect(actions.updateKeywords(keywords)).toEqual(expectedAction);
    });
 
    // …
});

那些测试用例的逻辑相当粗略,首先构建2个我们希望的结果,然后调用业务代码,最终验明正身工作代码的运行结果与希望是或不是同样。这正是写测试用例的着力套路。

大家在写测试用例时尽量保持用例的单纯职务,不要覆盖太多不一致的业务范围。测试用例数量能够有诸多少个,但各类都不应有很复杂。

1.相等断言

toBe(value): 相比数字、字符串
toEqual(value): 比较对象、数组
toBeNull()
toBeUndefined()

把toBe换到===效果是同样的。要是你想检查2个对象的值,那么就要用 toEqual
来顶替了:

reducers

接着是 reducers,照旧选用 redux-actionshandleActions 来编写
reducer,那里用表格的来做示范:

import { handleActions } from ‘redux-actions’; import Immutable from
‘seamless-immutable’; import * as type from ‘../types/bizTable’; /*
暗许状态 */ export const defaultState = Immutable({ loading: false,
pagination: { current: 1, pageSize: 15, total: 0 }, data: [] });
export default handleActions( { // … /* 处理获得多少成功 */
[type.BIZ_TABLE_GET_RES_SUCCESS]: (state, {payload}) => {
return state.merge( { loading: false, pagination: {total:
payload.total}, data: payload.items }, {deep: true} ); }, // … },
defaultState );

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import { handleActions } from ‘redux-actions’;
import Immutable from ‘seamless-immutable’;
import * as type from ‘../types/bizTable’;
 
/* 默认状态 */
export const defaultState = Immutable({
    loading: false,
    pagination: {
        current: 1,
        pageSize: 15,
        total: 0
    },
    data: []
});
 
export default handleActions(
    {
        // …
 
        /* 处理获得数据成功 */
        [type.BIZ_TABLE_GET_RES_SUCCESS]: (state, {payload}) => {
            return state.merge(
                {
                    loading: false,
                    pagination: {total: payload.total},
                    data: payload.items
                },
                {deep: true}
            );
        },
        
        // …
    },
    defaultState
);

此地的动静对象使用了 seamless-immutable

对于 reducer,大家任重(英文名:rèn zhòng)而道远测试多个地方:

  1. 对此未知的 action.type ,是不是能回到当前状态。
  2. 对此每种工作 type ,是还是不是都回去了通过正确处理的状态。

上边是指向以上两点的测试代码:

import * as type from ‘@/store/types/bizTable’; import reducer, {
defaultState } from ‘@/store/reducers/bizTable’; /* 测试 bizTable
reducer */ describe(‘bizTable reducer’, () => { /* 测试未钦赐 state
参数景况下回到当前缺省 state */ test(‘should return the default state’,
() => { expect(reducer(undefined, {type:
‘UNKNOWN’})).toEqual(defaultState); }); // … /* 测试处理通常数据结果
*/ test(‘should handle successful data response’, () => { /*
模拟重临数据结果 */ const payload = { items: [ {id: 1, code: ‘1’},
{id: 2, code: ‘2’} ], total: 2 }; /* 期望重临的事态 */ const
expectedState = defaultState .setIn([‘pagination’, ‘total’],
payload.total) .set(‘data’, payload.items) .set(‘loading’, false);
expect( reducer(defaultState, { type:
type.BIZ_TABLE_GET_RES_SUCCESS, payload }) ).toEqual(expectedState);
}); // … });

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import * as type from ‘@/store/types/bizTable’;
import reducer, { defaultState } from ‘@/store/reducers/bizTable’;
 
/* 测试 bizTable reducer */
describe(‘bizTable reducer’, () => {
    
    /* 测试未指定 state 参数情况下返回当前缺省 state */
    test(‘should return the default state’, () => {
        expect(reducer(undefined, {type: ‘UNKNOWN’})).toEqual(defaultState);
    });
    
    // …
    
    /* 测试处理正常数据结果 */
    test(‘should handle successful data response’, () => {
        /* 模拟返回数据结果 */
        const payload = {
            items: [
                {id: 1, code: ‘1’},
                {id: 2, code: ‘2’}
            ],
            total: 2
        };
        /* 期望返回的状态 */
        const expectedState = defaultState
            .setIn([‘pagination’, ‘total’], payload.total)
            .set(‘data’, payload.items)
            .set(‘loading’, false);
 
        expect(
            reducer(defaultState, {
                type: type.BIZ_TABLE_GET_RES_SUCCESS,
                payload
            })
        ).toEqual(expectedState);
    });
    
    // …
});

那边的测试用例逻辑也很简短,仍旧是地方断言期望结果的覆辙。上面是
selectors 的有的。

2.暗含断言

toHaveProperty(keyPath, value): 是或不是有相应的本性
toContain(item): 是或不是含有相应的值,括号里写上数组、字符串
toMatch(regexpOrString): 括号里写上正则

test(‘对象分配属性’,()=>{
    const data={one:1};
    data[‘two’]=2;
    expect(data).toEqual({one:1,two:2});
});

selectors

selector 的功力是获取对应业务的情景,那里运用了 reselect
来做缓存,防止 state 未变更的情状下再也总括,先看一下表格的 selector
代码:

import { createSelector } from ‘reselect’; import * as defaultSettings
from ‘@/utils/defaultSettingsUtil’; // … const getBizTableState =
(state) => state.bizTable; export const getBizTable =
createSelector(getBizTableState, (bizTable) => { return
bizTable.merge({ pagination: defaultSettings.pagination }, {deep:
true}); });

1
2
3
4
5
6
7
8
9
10
11
12
import { createSelector } from ‘reselect’;
import * as defaultSettings from ‘@/utils/defaultSettingsUtil’;
 
// …
 
const getBizTableState = (state) => state.bizTable;
 
export const getBizTable = createSelector(getBizTableState, (bizTable) => {
    return bizTable.merge({
        pagination: defaultSettings.pagination
    }, {deep: true});
});

此间的分页器部分参数在类型中是统一设置,所以 reselect
很好的形成了这么些工作:若是事情景况不变,直接回到上次的缓存。分页器暗中认可设置如下:

export const pagination = { size: ‘small’, showTotal: (total, range)
=> `${range[0]}-${range[1]} / ${total}`, pageSizeOptions:
[’15’, ’25’, ’40’, ’60’], showSizeChanger: true, showQuickJumper: true
};

1
2
3
4
5
6
7
export const pagination = {
    size: ‘small’,
    showTotal: (total, range) => `${range[0]}-${range[1]} / ${total}`,
    pageSizeOptions: [’15’, ’25’, ’40’, ’60’],
    showSizeChanger: true,
    showQuickJumper: true
};

那正是说大家的测试也主固然三个方面:

  1. 对于工作 selector ,是不是重回了不利的内容。
  2. 缓存功用是或不是正规。

测试代码如下:

import Immutable from ‘seamless-immutable’; import { getBizTable } from
‘@/store/selectors’; import * as defaultSettingsUtil from
‘@/utils/defaultSettingsUtil’; /* 测试 bizTable selector */
describe(‘bizTable selector’, () => { let state; beforeEach(() =>
{ state = createState(); /* 每一个用例执行前重置缓存总计次数 */
getBizTable.resetRecomputations(); }); function createState() { return
Immutable({ bizTable: { loading: false, pagination: { current: 1,
pageSize: 15, total: 0 }, data: [] } }); } /* 测试重回正确的 bizTable
state */ test(‘should return bizTable state’, () => { /* 业务意况ok 的 */ expect(getBizTable(state)).toMatchObject(state.bizTable); /*
分页暗许参数设置 ok 的 */ expect(getBizTable(state)).toMatchObject({
pagination: defaultSettingsUtil.pagination }); }); /* 测试 selector
缓存是不是行得通 */ test(‘check memoization’, () => {
getBizTable(state); /* 第二次总结,缓存计算次数为 1 */
expect(getBizTable.recomputations()).toBe(1); getBizTable(state); /*
业务处境不变的动静下,缓存总计次数应当照旧 1 */
expect(getBizTable.recomputations()).toBe(1); const newState =
state.setIn([‘bizTable’, ‘loading’], true); getBizTable(newState); /*
业务情形改变了,缓存总结次数应当是 2 了 */
expect(getBizTable.recomputations()).toBe(2); }); });

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import Immutable from ‘seamless-immutable’;
import { getBizTable } from ‘@/store/selectors’;
import * as defaultSettingsUtil from ‘@/utils/defaultSettingsUtil’;
 
/* 测试 bizTable selector */
describe(‘bizTable selector’, () => {
    
    let state;
 
    beforeEach(() => {
        state = createState();
        /* 每个用例执行前重置缓存计算次数 */
        getBizTable.resetRecomputations();
    });
 
    function createState() {
        return Immutable({
            bizTable: {
                loading: false,
                pagination: {
                    current: 1,
                    pageSize: 15,
                    total: 0
                },
                data: []
            }
        });
    }
 
    /* 测试返回正确的 bizTable state */
    test(‘should return bizTable state’, () => {
        /* 业务状态 ok 的 */
        expect(getBizTable(state)).toMatchObject(state.bizTable);
        
        /* 分页默认参数设置 ok 的 */
        expect(getBizTable(state)).toMatchObject({
            pagination: defaultSettingsUtil.pagination
        });
    });
 
    /* 测试 selector 缓存是否有效 */
    test(‘check memoization’, () => {
        getBizTable(state);
        /* 第一次计算,缓存计算次数为 1 */
        expect(getBizTable.recomputations()).toBe(1);
        
        getBizTable(state);
        /* 业务状态不变的情况下,缓存计算次数应该还是 1 */
        expect(getBizTable.recomputations()).toBe(1);
        
        const newState = state.setIn([‘bizTable’, ‘loading’], true);
        getBizTable(newState);
        /* 业务状态改变了,缓存计算次数应该是 2 了 */
        expect(getBizTable.recomputations()).toBe(2);
    });
});

测试用例依旧很简短有木有?保持那些点子就对了。上面来讲下多少有点复杂的地点,sa瓦斯部分。

3.逻辑断言

toBeTruthy()
toBeFalsy()
在JavaScript中,有六个falsy值:false0''null
undefined,和NaN。别的一切都以Truthy。

toBeGreaterThan(number): 大于
toBeLessThan(number): 小于

toEqual 会递归的检查数;组或对象的每1个地点。

sagas

那边自个儿用了 redux-saga 处理业务流,那里具体也正是异步调用 api
请求数据,处理成功结果和错误结果等。

只怕部分童鞋觉得搞这么复杂干嘛,异步请求用个 redux-thunk
不就完事了吧?别急,耐心看完你就知道了。

此处有必不可少大约介绍下 redux-saga 的干活办法。saga 是一种 es6
的生成器函数 – Generator ,大家选拔他来发生各个评释式的 effects ,由
redux-saga 引擎来消化处理,带动工作展开。

那里我们来看望获取表格数据的政工代码:

import { all, takeLatest, put, select, call } from ‘redux-saga/effects’;
import * as type from ‘../types/bizTable’; import * as actions from
‘../actions/bizTable’; import { getBizToolbar, getBizTable } from
‘../selectors’; import * as api from ‘@/services/bizApi’; // … export
function* onGetBizTableData() { /* 先获取 api
调用要求的参数:关键字、分页音信等 */ const {keywords} = yield
select(getBizToolbar); const {pagination} = yield select(getBizTable);
const payload = { keywords, paging: { skip: (pagination.current – 1) *
pagination.pageSize, max: pagination.pageSize } }; try { /* 调用 api
*/ const result = yield call(api.getBizTableData, payload); /*
符合规律重回 */ yield put(actions.putBizTableDataSuccessResult(result)); }
catch (err) { /* 错误再次来到 */ yield
put(actions.putBizTableDataFailResult()); } }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import { all, takeLatest, put, select, call } from ‘redux-saga/effects’;
import * as type from ‘../types/bizTable’;
import * as actions from ‘../actions/bizTable’;
import { getBizToolbar, getBizTable } from ‘../selectors’;
import * as api from ‘@/services/bizApi’;
 
// …
 
export function* onGetBizTableData() {
    /* 先获取 api 调用需要的参数:关键字、分页信息等 */
    const {keywords} = yield select(getBizToolbar);
    const {pagination} = yield select(getBizTable);
 
    const payload = {
        keywords,
        paging: {
            skip: (pagination.current – 1) * pagination.pageSize, max: pagination.pageSize
        }
    };
 
    try {
        /* 调用 api */
        const result = yield call(api.getBizTableData, payload);
        /* 正常返回 */
        yield put(actions.putBizTableDataSuccessResult(result));
    } catch (err) {
        /* 错误返回 */
        yield put(actions.putBizTableDataFailResult());
    }
}

不熟悉 redux-saga
的童鞋也并非太在意代码的切实写法,看注释应该能明白这一个工作的具体步骤:

  1. 从对应的 state 里取到调用 api
    时必要的参数部分(搜索关键字、分页),那里调用了刚刚的 selector。
  2. 整合好参数并调用对应的 api 层。
  3. 比方常常再次来到结果,则发送成功 action 通告 reducer 更新意况。
  4. 一经不当重返,则发送错误 action 文告 reducer。

那么具体的测试用例应该怎么写吧?大家都知道这种事情代码涉及到了 api
或别的层的调用,要是要写单元测试必须做一些 mock 之类来幸免真正调用 api
层,上边大家来看一下 怎么针对这么些 saga 来写测试用例:

import { put, select } from ‘redux-saga/effects’; // … /*
测试获取数据 */ test(‘request data, check success and fail’, () => {
/* 当前的事情景况 */ const state = { bizToolbar: { keywords: ‘some
keywords’ }, bizTable: { pagination: { current: 1, pageSize: 15 } } };
const gen = cloneableGenerator(saga.onGetBizTableData)(); /* 1.
是或不是调用了正确的 selector 来取得请求时要发送的参数 */
expect(gen.next().value).toEqual(select(getBizToolbar));
expect(gen.next(state.bizToolbar).value).toEqual(select(getBizTable));
/* 2. 是不是调用了 api 层 */ const callEffect =
gen.next(state.bizTable).value;
expect(callEffect[‘CALL’].fn).toBe(api.getBizTableData); /* 调用 api
层参数是还是不是传递正确 */ expect(callEffect[‘CALL’].args[0]).toEqual({
keywords: ‘some keywords’, paging: {skip: 0, max: 15} }); /* 3.
模拟正确重返分支 */ const successBranch = gen.clone(); const successRes
= { items: [ {id: 1, code: ‘1’}, {id: 2, code: ‘2’} ], total: 2 };
expect(successBranch.next(successRes).value).toEqual(
put(actions.putBizTableDataSuccessResult(successRes)));
expect(successBranch.next().done).toBe(true); /* 4. 效仿错误再次来到分支
*/ const failBranch = gen.clone(); expect(failBranch.throw(new
Error(‘模拟产生12分’)).value).toEqual(
put(actions.putBizTableDataFailResult()));
expect(failBranch.next().done).toBe(true); });

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import { put, select } from ‘redux-saga/effects’;
 
// …
 
/* 测试获取数据 */
test(‘request data, check success and fail’, () => {
    /* 当前的业务状态 */
    const state = {
        bizToolbar: {
            keywords: ‘some keywords’
        },
        bizTable: {
            pagination: {
                current: 1,
                pageSize: 15
            }
        }
    };
    const gen = cloneableGenerator(saga.onGetBizTableData)();
 
    /* 1. 是否调用了正确的 selector 来获得请求时要发送的参数 */
    expect(gen.next().value).toEqual(select(getBizToolbar));
    expect(gen.next(state.bizToolbar).value).toEqual(select(getBizTable));
 
    /* 2. 是否调用了 api 层 */
    const callEffect = gen.next(state.bizTable).value;
    expect(callEffect[‘CALL’].fn).toBe(api.getBizTableData);
    /* 调用 api 层参数是否传递正确 */
    expect(callEffect[‘CALL’].args[0]).toEqual({
        keywords: ‘some keywords’,
        paging: {skip: 0, max: 15}
    });
 
    /* 3. 模拟正确返回分支 */
    const successBranch = gen.clone();
    const successRes = {
        items: [
            {id: 1, code: ‘1’},
            {id: 2, code: ‘2’}
        ],
        total: 2
    };
    expect(successBranch.next(successRes).value).toEqual(
        put(actions.putBizTableDataSuccessResult(successRes)));
    expect(successBranch.next().done).toBe(true);
 
    /* 4. 模拟错误返回分支 */
    const failBranch = gen.clone();
    expect(failBranch.throw(new Error(‘模拟产生异常’)).value).toEqual(
        put(actions.putBizTableDataFailResult()));
    expect(failBranch.next().done).toBe(true);
});

以此测试用例相比较前面包车型大巴复杂性了一部分,大家先来说下测试 saga 的法则。前边说过
saga 实际上是回去种种申明式的 effects
,然后由引擎来真的进行。所以我们测试的指标正是要看 effects
的发出是还是不是切合预期。那么effect
到底是个神马东西吧?其实正是字面量对象!

小编们可以用在业务代码同样的方法来爆发这个字面量对象,对于字面量对象的断言就格外简单了,并且没有一贯调用
api 层,就富余做 mock
咯!那些测试用例的步子正是使用生成器函数一步步的发生下叁个 effect
,然后断言比较。

从地点的笺注 三 、4 能够见见,redux-saga
还提供了一些扶植函数来便宜的处理分支断点。

这也是自家选择 redux-saga 的缘由:强大并且有利于测试。

4.not

取反,用法见下边例子

您同一能够实行反向测试:

api 和 fetch 工具库

接下去就是api 层相关的了。前面讲过调用后台请求是用的 fetch
,小编封装了多少个法子来简化调用和结果处理:getJSON()postJSON()
,分别对应 GET 、POST 请求。先来探视 api 层代码:

import { fetcher } from ‘@/utils/fetcher’; export function
getBizTableData(payload) { return fetcher.postJSON(‘/api/biz/get-table’,
payload); }

1
2
3
4
5
import { fetcher } from ‘@/utils/fetcher’;
 
export function getBizTableData(payload) {
    return fetcher.postJSON(‘/api/biz/get-table’, payload);
}

事情代码很简短,那么测试用例也非常的粗略:

import sinon from ‘sinon’; import { fetcher } from ‘@/utils/fetcher’;
import * as api from ‘@/services/bizApi’; /* 测试 bizApi */
describe(‘bizApi’, () => { let fetcherStub; beforeAll(() => {
fetcherStub = sinon.stub(fetcher); }); // … /* getBizTableData api
应该调用正确的 method 和传递正确的参数 */ test(‘getBizTableData api
should call postJSON with right params of fetcher’, () => { /*
模拟参数 */ const payload = {a: 1, b: 2}; api.getBizTableData(payload);
/* 检查是或不是调用了工具库 */
expect(fetcherStub.postJSON.callCount).toBe(1); /* 检查调用参数是还是不是科学
*/
expect(fetcherStub.postJSON.lastCall.calledWith(‘/api/biz/get-table’,
payload)).toBe(true); }); });

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import sinon from ‘sinon’;
import { fetcher } from ‘@/utils/fetcher’;
import * as api from ‘@/services/bizApi’;
 
/* 测试 bizApi */
describe(‘bizApi’, () => {
    
    let fetcherStub;
 
    beforeAll(() => {
        fetcherStub = sinon.stub(fetcher);
    });
 
    // …
 
    /* getBizTableData api 应该调用正确的 method 和传递正确的参数 */
    test(‘getBizTableData api should call postJSON with right params of fetcher’, () => {
        /* 模拟参数 */
        const payload = {a: 1, b: 2};
        api.getBizTableData(payload);
 
        /* 检查是否调用了工具库 */
        expect(fetcherStub.postJSON.callCount).toBe(1);
        /* 检查调用参数是否正确 */
        expect(fetcherStub.postJSON.lastCall.calledWith(‘/api/biz/get-table’, payload)).toBe(true);
    });
});

是因为 api 层直接调用了工具库,所以那里用 sinon.stub()
来替换工具库达到测试目标。

接着便是测试自个儿包装的 fetch 工具库了,那里 fetch 笔者是用的
isomorphic-fetch ,所以接纳了 nock 来模拟 Server
举行测试,重假若测试不奇怪访问回到结果和宪章服务器非常等,示例片段如下:

import nock from ‘nock’; import { fetcher, FetchError } from
‘@/utils/fetcher’; /* 测试 fetcher */ describe(‘fetcher’, () => {
afterEach(() => { nock.cleanAll(); }); afterAll(() => {
nock.restore(); }); /* 测试 getJSON 获得平时数据 */ test(‘should get
success result’, () => { nock(”) .get(‘/test’)
.reply(200, {success: true, result: ‘hello, world’}); return
expect(fetcher.getJSON(‘);
}); // … /* 测试 getJSON 捕获 server 大于 400 的拾贰分情形 */
test(‘should catch server status: 400+’, (done) => { const status =
500; nock(”) .get(‘/test’) .reply(status);
fetcher.getJSON(‘) => {
expect(error).toEqual(expect.any(FetchError));
expect(error).toHaveProperty(‘detail’);
expect(error.detail.status).toBe(status); done(); }); }); /* 测试
getJSON 传递正确的 headers 和 query strings */ test(‘check headers and
query string of getJSON()’, () => { nock(”, { reqheaders:
{ ‘Accept’: ‘application/json’, ‘authorization’: ‘Basic Auth’ } })
.get(‘/test’) .query({a: ‘123’, b: 456}) .reply(200, {success: true,
result: true}); const headers = new Headers();
headers.append(‘authorization’, ‘Basic Auth’); return
expect(fetcher.getJSON( ”, {a: ‘123’, b: 456},
headers)).resolves.toBe(true); }); // … });

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
import nock from ‘nock’;
import { fetcher, FetchError } from ‘@/utils/fetcher’;
 
/* 测试 fetcher */
describe(‘fetcher’, () => {
 
    afterEach(() => {
        nock.cleanAll();
    });
 
    afterAll(() => {
        nock.restore();
    });
 
    /* 测试 getJSON 获得正常数据 */
    test(‘should get success result’, () => {
        nock(‘http://some’)
            .get(‘/test’)
            .reply(200, {success: true, result: ‘hello, world’});
 
        return expect(fetcher.getJSON(‘http://some/test’)).resolves.toMatch(/^hello.+$/);
    });
 
    // …
 
    /* 测试 getJSON 捕获 server 大于 400 的异常状态 */
    test(‘should catch server status: 400+’, (done) => {
        const status = 500;
        nock(‘http://some’)
            .get(‘/test’)
            .reply(status);
 
        fetcher.getJSON(‘http://some/test’).catch((error) => {
            expect(error).toEqual(expect.any(FetchError));
            expect(error).toHaveProperty(‘detail’);
            expect(error.detail.status).toBe(status);
            done();
        });
    });
 
   /* 测试 getJSON 传递正确的 headers 和 query strings */
    test(‘check headers and query string of getJSON()’, () => {
        nock(‘http://some’, {
            reqheaders: {
                ‘Accept’: ‘application/json’,
                ‘authorization’: ‘Basic Auth’
            }
        })
            .get(‘/test’)
            .query({a: ‘123’, b: 456})
            .reply(200, {success: true, result: true});
 
        const headers = new Headers();
        headers.append(‘authorization’, ‘Basic Auth’);
        return expect(fetcher.getJSON(
            ‘http://some/test’, {a: ‘123’, b: 456}, headers)).resolves.toBe(true);
    });
    
    // …
});

主导也没怎么复杂的,首要注意 fetch 是 promise 重回,jest
的各类异步测试方案都能很好满意。

剩余的局地就是跟 UI 相关的了。

例:

test(‘matchers’,()=>{
    const a = {
        hello: ‘jest’,
        hi :{
            name: ‘jest’
    }
}
const b = {
    hello: ‘jest’,
    hi: {
        name: ‘jest’
    }
}

// 以下结果均为true

expect(a).toEqual(b)
expect([1,2,3]).toEqual([1,2,3])
expect(null).toBeNull()

expect([1,2,3]).toContain(1)
expect(b).toHaveProperty(‘hi’)
expect(‘123’).toContain(‘2’)
expect(‘123’).toMatch(/^\d+$/)

expect(‘123’).not.toContain(‘4’)
})

  • 使用npx jest测试执行,结果为passed

附:法定文书档案

test(‘正数相加不为零’,()=>{
    for(let a=1;a<10;a++){
        for(let b=1;b<10;b++){
            expect(a+b).not.toBe(0);
        }
    }
});

容器组件

容器组件的要紧目标是传递 state 和 actions,看下工具栏的器皿组件代码:

import { connect } from ‘react-redux’; import { getBizToolbar } from
‘@/store/selectors’; import * as actions from
‘@/store/actions/bizToolbar’; import BizToolbar from
‘@/components/BizToolbar’; const mapStateToProps = (state) => ({
…getBizToolbar(state) }); const mapDispatchToProps = { reload:
actions.reload, updateKeywords: actions.updateKeywords }; export default
connect(mapStateToProps, mapDispatchToProps)(BizToolbar);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { connect } from ‘react-redux’;
import { getBizToolbar } from ‘@/store/selectors’;
import * as actions from ‘@/store/actions/bizToolbar’;
import BizToolbar from ‘@/components/BizToolbar’;
 
const mapStateToProps = (state) => ({
    …getBizToolbar(state)
});
 
const mapDispatchToProps = {
    reload: actions.reload,
    updateKeywords: actions.updateKeywords
};
 
export default connect(mapStateToProps, mapDispatchToProps)(BizToolbar);

那便是说测试用例的目标也是反省那几个,那里运用了 redux-mock-store 来模拟
redux 的 store :

import React from ‘react’; import { shallow } from ‘enzyme’; import
configureStore from ‘redux-mock-store’; import BizToolbar from
‘@/containers/BizToolbar’; /* 测试容器组件 BizToolbar */
describe(‘BizToolbar container’, () => { const initialState = {
bizToolbar: { keywords: ‘some keywords’ } }; const mockStore =
configureStore(); let store; let container; beforeEach(() => { store
= mockStore(initialState); container = shallow(); }); /* 测试 state 到
props 的照耀是不是正确 */ test(‘should pass state to props’, () => {
const props = container.props();
expect(props).toHaveProperty(‘keywords’,
initialState.bizToolbar.keywords); }); /* 测试 actions 到 props
的照射是或不是科学 */ test(‘should pass actions to props’, () => { const
props = container.props(); expect(props).toHaveProperty(‘reload’,
expect.any(Function)); expect(props).toHaveProperty(‘updateKeywords’,
expect.any(Function)); }); });

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import React from ‘react’;
import { shallow } from ‘enzyme’;
import configureStore from ‘redux-mock-store’;
import BizToolbar from ‘@/containers/BizToolbar’;
 
/* 测试容器组件 BizToolbar */
describe(‘BizToolbar container’, () => {
    
    const initialState = {
        bizToolbar: {
            keywords: ‘some keywords’
        }
    };
    const mockStore = configureStore();
    let store;
    let container;
 
    beforeEach(() => {
        store = mockStore(initialState);
        container = shallow();
    });
 
    /* 测试 state 到 props 的映射是否正确 */
    test(‘should pass state to props’, () => {
        const props = container.props();
 
        expect(props).toHaveProperty(‘keywords’, initialState.bizToolbar.keywords);
    });
 
    /* 测试 actions 到 props 的映射是否正确 */
    test(‘should pass actions to props’, () => {
        const props = container.props();
 
        expect(props).toHaveProperty(‘reload’, expect.any(Function));
        expect(props).toHaveProperty(‘updateKeywords’, expect.any(Function));
    });
});

非常粗略有木有,所以也没啥可说的了。

许多时候测试须求区分
undefined,null和false,然而你有不想做那几个坚苦的事。那jest已经为您准备好了累累帮助办公室方法律专科学校门消除这一个标题。
— toBeNull 只匹配 null
— toBeUndefined 只匹配 undefined
— toBeDefined 是 toBeUndefined 的反匹配
— toBeTruthy 匹配 if 语句期望获取 true 的
— toBeFalsy 匹配 if 语句期望获取 false 的

UI 组件

此间以表格组件作为示范,大家将从来来看测试用例是怎么写。一般的话 UI
组件大家重点测试以下多少个方面:

  • 是还是不是渲染了合情合理的 DOM 结构
  • 体制是不是科学
  • 事情逻辑触发是还是不是科学

上面是测试用例代码:

JavaScript

import React from ‘react’; import { mount } from ‘enzyme’; import sinon
from ‘sinon’; import { Table } from ‘antd’; import * as
defaultSettingsUtil from ‘@/utils/defaultSettingsUtil’; import BizTable
from ‘@/components/BizTable’; /* 测试 UI 组件 BizTable */
describe(‘BizTable component’, () => { const defaultProps = {
loading: false, pagination: Object.assign({}, { current: 1, pageSize:
15, total: 2 }, defaultSettingsUtil.pagination), data: [{id: 1}, {id:
2}], getData: sinon.fake(), updateParams: sinon.fake() }; let
defaultWrapper; beforeEach(() => { defaultWrapper =
mount(<BizTable {…defaultProps}/>); }); // … /*
测试是或不是渲染了天经地义的功用子组件 */ test(‘should render table and
pagination’, () => { /* 是还是不是渲染了 Table 组件 */
expect(defaultWrapper.find(Table).exists()).toBe(true); /* 是还是不是渲染了
分页器 组件,样式是不是正确(mini) */
expect(defaultWrapper.find(‘.ant-table-pagination.mini’).exists()).toBe(true);
}); /* 测试第一回加载时数据列表为空是不是发起加载数据请求 */ test(‘when
componentDidMount and data is empty, should getData’, () => {
sinon.spy(BizTable.prototype, ‘componentDidMount’); const props =
Object.assign({}, defaultProps, { pagination: Object.assign({}, {
current: 1, pageSize: 15, total: 0 }, defaultSettingsUtil.pagination),
data: [] }); const wrapper = mount(<BizTable {…props}/>);
expect(BizTable.prototype.componentDidMount.calledOnce).toBe(true);
expect(props.getData.calledOnce).toBe(true);
BizTable.prototype.componentDidMount.restore(); }); /* 测试 table
翻页后是或不是科学触发 updateParams */ test(‘when change pagination of
table, should updateParams’, () => { const table =
defaultWrapper.find(Table); table.props().onChange({current: 2,
pageSize: 25}); expect(defaultProps.updateParams.lastCall.args[0])
.toEqual({paging: {current: 2, pageSize: 25}}); }); });

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
import React from ‘react’;
import { mount } from ‘enzyme’;
import sinon from ‘sinon’;
import { Table } from ‘antd’;
import * as defaultSettingsUtil from ‘@/utils/defaultSettingsUtil’;
import BizTable from ‘@/components/BizTable’;
 
/* 测试 UI 组件 BizTable */
describe(‘BizTable component’, () => {
    
    const defaultProps = {
        loading: false,
        pagination: Object.assign({}, {
            current: 1,
            pageSize: 15,
            total: 2
        }, defaultSettingsUtil.pagination),
        data: [{id: 1}, {id: 2}],
        getData: sinon.fake(),
        updateParams: sinon.fake()
    };
    let defaultWrapper;
 
    beforeEach(() => {
        defaultWrapper = mount(<BizTable {…defaultProps}/>);
    });
 
    // …
 
    /* 测试是否渲染了正确的功能子组件 */
    test(‘should render table and pagination’, () => {
        /* 是否渲染了 Table 组件 */
        expect(defaultWrapper.find(Table).exists()).toBe(true);
        /* 是否渲染了 分页器 组件,样式是否正确(mini) */
        expect(defaultWrapper.find(‘.ant-table-pagination.mini’).exists()).toBe(true);
    });
 
    /* 测试首次加载时数据列表为空是否发起加载数据请求 */
    test(‘when componentDidMount and data is empty, should getData’, () => {
        sinon.spy(BizTable.prototype, ‘componentDidMount’);
        const props = Object.assign({}, defaultProps, {
            pagination: Object.assign({}, {
                current: 1,
                pageSize: 15,
                total: 0
            }, defaultSettingsUtil.pagination),
            data: []
        });
        const wrapper = mount(<BizTable {…props}/>);
 
        expect(BizTable.prototype.componentDidMount.calledOnce).toBe(true);
        expect(props.getData.calledOnce).toBe(true);
        BizTable.prototype.componentDidMount.restore();
    });
 
    /* 测试 table 翻页后是否正确触发 updateParams */
    test(‘when change pagination of table, should updateParams’, () => {
        const table = defaultWrapper.find(Table);
        table.props().onChange({current: 2, pageSize: 25});
        expect(defaultProps.updateParams.lastCall.args[0])
            .toEqual({paging: {current: 2, pageSize: 25}});
    });
});

得益于设计分层的客体,大家很简单选择构造 props 来达到测试目标,结合
enzymesinon ,测试用例依旧维持简单的旋律。

举个例证:

总结

如上正是以此场景完整的测试用例编写思路和演示代码,文中提及的思路方法也统统能够用在
VueAngular 项目上。完整的代码内容在
这里
(首要的作业多说一回,各位童鞋觉得好扶持去给个 哈)。

说到底我们能够动用覆盖率来看下用例的遮盖程度是不是充裕(一般的话不要刻意追求
百分百,根据真实意况来定):
澳门葡京 4

单元测试是 TDD
测试驱动开发的底蕴。从上述全部经过能够观察,好的布署分层是很不难编写测试用例的,单元测试不单单只是为了保障代码品质:他会逼着你考虑代码设计的创设,拒绝面条代码

借用 Clean Code 的甘休语:

贰零零陆 年,在加入于Tallinn进行的短平快大会时,埃利sabeth Hedrickson
递给自家一条看似 Lance Armstrong热销的那种浅绛紫腕带。这条腕带地方写着“沉迷测试”(Test
Obsessed)的字样。笔者喜欢地戴上,并自豪地一向系着。自从 1998 年从 KentBeck 那儿学到 TDD 以来,小编的确迷上了测试驱动开发。

不过随着就发出了些奇事。我发现自个儿不能取下腕带。不仅是因为腕带很紧,而且那也是条精神上的羁绊。这腕带正是本身职业道德的透露,也是本身答应尽己所能写出最好代码的唤起。取下它,就像正是反其道而行之了这一个发表和承诺似的。

从而它还在自个儿的手腕上。在写代码时,笔者用余光瞟见它。它间接提示本人,笔者做了写出清新代码的允诺。

1 赞 1 收藏
评论

澳门葡京 5

test(‘null’,()=>{
    const n=null;
    expect(n).toBeNull();
    expect(n).toBeDefined();
    expect(n).not.toBeUndefined();
    expect(n).not.toBeTruthy();
    expect(n).toBeFalsy();
});
test(‘0’,()=>{
    const z=0;
    expect(z).not.toBeNull();
    expect(z).toBeDefined();
    expect(z).not.toBeUndefined();
    expect(z).not.toBeTruthy();
    expect(z).toBeFalsy();
});

您应有根据你的代码来挑选最佳的匹配器。

数字的可比都用相应的匹配器。

test(‘2加2’,()=>{
    const value=2+2;
    expect(value).toBeGreaterThan(3);//大于匹配器
    expect(value).toBeGreaterThanOrEqual(3.5);//大于或等于匹配器
    expect(value).toBeLessThan(5);//小于匹配器
    expect(value).toBeLessThanOrEqual(4.5);//小于或等于匹配器
    // toBe 和 toEqual 对数字来说效果卓殊
    expect(value).toBe(4);
    expect(value).toEqual(4);
});

假若你不期望浮点数的测试存在误差,请使用 toBeCloseTo 来替代 toEqual。

test(‘浮点数相加’,()=>{
    const value=0.1+0.2;
    expect(value).not.toBe(0.3);// 错误的点子,因为存在误差。
    expect(value).toBeCloseTo(0.3);// 正确的章程。
});

对于字符串你能够选择 toMatch 同盟正则表达式。

test(‘team 中没有 i ‘,()=>{
    expect(‘team’).not.toMatch(/I/);
});
test(‘但是 Christoph 中有 stop’,()=>{
    expect(‘Christoph’).toMatch(/stop/);
});

测试数组是不是包蕴特定的要素得以动用 toContain。

const shoppingList=[
    ‘diapers’,
    ‘kleenex’,
    ‘trash bags’,
    ‘paper towels’,
    ‘beer’,
];
test(‘shopping 列队是有 beer 的’,()=>{
    expect(shoppingList).toContain(‘beer’);
});

万一您想测试有些函数被调用的时候是或不是会抛出荒唐,请使用 toThrow。

function compileAndroidCode(){
    throw new ConfigError(‘你利用了不当的JDK’);
}
test(‘编译 android’,()=>{
    expect(compileAndroidCode).toThrow();
    expect(compileAndroidCode).toThrow(ConfigError);
    // 你还能配合完整的错误音讯或正则表达式
    expect(compileAndroidCode).toThrow(‘你利用了不当的JDK’);
    expect(compileAndroidCode).toThrow(/JDK/);
});

相关文章

发表评论

电子邮件地址不会被公开。 必填项已用*标注

*
*
Website