看这一篇就够了,前端自动化单元测试

在 2017 年攻读 React + Redux 的一部分提出(下篇)

2017/09/11 · JavaScript
· React,
Redux

原稿出处: 郭永峰   

在那里说一下前端开发的一个风味是越多的会提到用户界面,当开发规模高达自然水日常,大致决定了其复杂度会倍增的增进。

前言

乘势Web业务的日益复杂化和多元化,前端开发也有了前者工程化的定义,前端工程化成为当下前端架构中重点的一环,本质上也是软件工程的一种,因而大家供给从软件工程的角度来切磋前端工程,而自动化测试则是软件工程中一言九鼎的一环。本文就钻研一下前端领域中的自动化测试,以及哪些执行。

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

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

看这一篇就够了,前端自动化单元测试。初稿出处: deepfunc   

乘胜 Web
应用的复杂程度越来越高,很多商厦进而讲究前者单元测试。大家看出的大部学科都会讲单元测试的基本点、一些有代表性的测试框架
api
怎么利用,但在事实上项目中单元测试要怎么出手?测试用例应该包蕴如何具体内容呢?

正文从二个真真的运用场景出发,从设计方式、代码结构来分析单元测试应该包涵哪些内容,具体测试用例怎么写,希望看到的童鞋都能具备收获。

澳门葡京 1关于测试的片段上学提议

作者们得以结合使用一些测试工具来救助测试 JS 代码,一般选拔 Mocha/Chai
或是 Karma/Jasmine 。而只要当你想测试 angular
的代码时,你会意识还有愈来愈多的测试工具。可是对于 React
应用的测试,相比推荐应用 Airbnb 团队产品的
anzyme
来拓展零部件的测试,以保住组件的安澜可相信,近来应用尤其常见;而另一种方法是运用
Facebook 的 jest 来拓展测试。

恐怕过多同学都觉得应该选拔上述的某1个测试库来展开测试工作,可是,你也足以将
anzymejest 结合起来共同行使。越发是在实行一些 snapshot
快照测试的时候,二种都以填补的,它们已经是 React
应用测试中山高校家公认的标准库了。

sinon
也是个要命精良的测试帮忙理工科程师具,能够支持大家在 spy、stub、mock
等测试阶段提供相应的工具支持测试。倘使对那多少个概念不老聃晰,能够看看那里。

别的,在此处给您隆重的给你推荐一篇 A. Sharif 写的 Some Thoughts On
Testing React/Redux
Applications,满满的干货分享哦。

不论是在代码的上马搭建进程中,照旧后来难以免止的重构和校正bug进程中,平时会沦为逻辑难以梳理、不可能明白全局关联的地步。

如何是单元测试

单元测试(unit
testing),是指对软件中的最小可测试单元实行检查和验证。对于单元测试中单元的意义,一般的话,要依照实情去看清其具体意思,如C语言中单元指三个函数,Java里单元指二个类,图形化的软件中得以指1个窗口或2个菜单等。总的来说,单元便是人为规定的蝇头的被测功效模块。单元测试是在软件开发进程中要实行的最低级别的测试活动,软件的独立单元将在与程序的其他部分相隔绝的状态下展开测试。——百度百科

品类用到的技术框架

该类型利用 react
技术栈,用到的重要框架包含:reactreduxreact-reduxredux-actionsreselectredux-sagaseamless-immutableantd

多一些零部件的单元测试,少一些集成测试

Enzyme
能够补助我们贯彻组件的单元测试和合并测试。那里大家得以由此两种方法来渲染组件:

  • shallow()
  • mount()
  • render()

shallow() 只可以用来渲染不分包 children 的机件,mount()
则能够渲染全数的子组件。所以单元测试的时候能够选择 shallow()
mount()
则一般用于集成测试。集成测试往往是很不难被切断的,因为她供给测试由一组或是多个零部件树组合的风貌,所以集成测试一般维护资金是相比较高的。因而大家能够多做一些精密的单元测试,少做一些主要的合一测试。

其三种测试的措施是运用 render() 方法,具有类似
mount()艺术的作用,然则 mount() 能够访问到零部件的生命周期方法,比如
componentDidUpdate等。

正如这么些 issue 中提议的 API differences between render and
mount/shallow,能够计算出:

  • 使用 shallow 开首测试用例
  • 如果 componentDidMount or componentDidUpdate
    等艺术也供给测试,那么使用 mount 方法吧
  • 一经要测试组件生命周期方法、子组件的行为等,使用 mount 方法吧
  • 要是想更高质量的测试子组件,并且对组件的生命周期等办法有个别关怀,那么使用
    render 方法吧

澳门葡京 2

为啥要测试

原先没有编写制定和掩护测试用例的习惯,在品种的烦乱开发周期中也没时间去做那么些工作,相信有诸多开发职员都不会重视单元测试那项工作。在真的写了一段时间基础零部件后,才发觉自动化测试有不少功利:

  1. 升级代码品质。虽不可能说百分百无bug,但最少表明测试用例覆盖到的气象是从未难题的。
  2. 能便捷反馈,能明确UI组件工作情景是不是符合本人预想。
  3. 开发者会更加相信本身的代码,也不会害怕将代码交给旁人维护。后人接手一段有测试用例的代码,修改起来也会愈来愈从容。测试用例里格外理解的论述了开发者和使用者对于那段代码的愿意和要求,也尤其便宜代码的传承。

当然由于保养测试用例也是一大笔费用,照旧要根据投入产出比来做单元测试。对于像基础零部件、基础模型之类的不常变更且复用较多的局地,能够考虑写测试用例来保障品质,但对于迭代较快的工作逻辑及生活时间不短的一部分就没须求浪费时间了。

故此github上来看的star较多的牛逼开源前端项目基本上都以有测试代码的,看来产业界大牛们都以相比较偏重单元测试这块的。

选择场景介绍

澳门葡京 3

本条利用场景从 UI 层来讲至关心注重要由八个部分构成:

  • 工具栏,包括刷新按钮、关键字搜索框
  • 报表体现,选择分页的款式浏览

见到那里有的童鞋恐怕会说:切!这么简单的界面和业务逻辑,依然真正意况呢,还亟需写神马单元测试吗?

别急,为了保障小说的读书体验和尺寸适中,能讲通晓难题的凝练场景正是好光景不是吗?慢慢往下看。

确定保证测试用例不难、最小颗粒度

要不然的话你需求为此付出很高的保险开支。

确认各类组件是还是不是都有履行过单元测试,确认各种 props 和 callbacks
都在合龙测试的时候传递给了相应的子组件。

为了保障组件测试用例的小颗粒度和简单化,你要求熟习一下
selectorsEnzyme 提供了充裕的
selector 去深切组件树。

其余,提出使用 sinon 来测试 callbacks
回调函数,不要在组件中测试工作逻辑,那真不是个好注意。而是应该将工作逻辑从组件中解耦并对其展开测试。

最后,推特(TWTR.US) 出品的 Jest
也能在初期扶助大家特别轻量的施行测试,你能够分外不难就安装好 snapshot
test,那样当组件的出口改变的话测试用例会自动的报出失利的结果,并且能够取获得错误新闻。

而单元测试作为一种“言必有中、保驾护航”的底蕴手段,为开发提供了“围墙和脚手架”,可以有效的创新这么些题材。

连带概念

设计情势与结构分析

在这一个情况设计开发中,大家严刻遵从 redux 单向数据流 与 react-redux
的最佳实践,并选用 redux-saga 来处理业务流,reselect
来处理景况缓存,通过 fetch 来调用后台接口,与忠实的品类并未差别。

分段设计与代码协会如下所示:
澳门葡京 4

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

切切实实的代码请看 这里。

拥抱 TDD(测试驱动开发)

具有的人都可能会对你说:你应当按测试驱动的情势来开展开发。可是,差不多没多少人会那样,项目需求如山的积压,上线的剧本急切火燎,测试驱动?玩吗?!大概大部分小伙伴都是这么的心声。

而是,假设你能够清晰的在 React + Redux
的使用中应用相应的测试方案对各类部分都开始展览测试,你就可见丰硕轻松的落实TDD。纵然你会发现 reducer
的测试和零部件的测试是很区别等的,但骨子里每连串型(reducer、component、….
)的测试情势其实都以平等的。

就拿 reducer 的测试为例吧,一般是梦想
reducer(state, action) === newState,其实那种艺术和
(input) => output 的方式是同等的。假如你要测试 state
的不可变性的话,建议您能够采纳
deep-freeze,能够看下以下示例代码:

JavaScript

import deepFreeze from ‘deep-freeze’ const initialState = { … }; const
action = { type: …, payload: … }; const expectedState = { … };
deepFreeze(initialState); expect(reducer(initialState,
action)).to.equal(expectedState);

1
2
3
4
5
6
7
8
9
import deepFreeze from ‘deep-freeze’
 
const initialState = { … };
const action = { type: …, payload: … };
const expectedState = { … };
 
deepFreeze(initialState);
 
expect(reducer(initialState, action)).to.equal(expectedState);

借使您可见很清楚的通晓如何测试应用中的每一个片段,那就最好使用 TDD。

作为一种经典的支出和重构手段,单元测试在软件开发领域被普遍认可和使用;前端领域也稳步积淀起了丰裕的测试框架和极品实践。

TDD

TDD是Test Driven Development 的缩写,相当于测试驱动开发。

万般古板软件工程将测试描述为软件生命周期的八个环节,并且是在编码之后。但相当慢开发大师KentBeck在2002年出版了 Test Driven Development By Example
一书,从而确立了测试驱动开发这么些圈子。

TDD要求依据如下规则:

  • 写多少个单元测试去描述程序的3个上面。
  • 运行它应该会战败,因为程序还缺少那性子格。
  • 为那么些顺序添加一些竭尽不难的代码保险测试通过。
  • 重构那某个代码,直到代码没有重新、代码权利清晰并且协会简单。
  • 绵绵重复这么做,积累代码。

TDD具有很强的指标性,在一贯结果的点拨下开产生产代码,然后不断缠绕那么些指标去改正代码,其优势是急速和去冗余的。所以其性格应该是由须求得出测试,由测试代码得出生产代码。打个假若就像自行车的多个轮子,纵然都以在向同1个趋势转动,不过后轮是施力的,带轻轨子前行,而前轮是受力的,被向前的单车拉动而转。

单元测试部分介绍

先讲一下用到了怎样测试框架和工具,主要内容囊括:

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

借使有童鞋对上边这几个使用和配备不熟的话,直接看官方文书档案吧,比其余学科都写的好。

接下去,大家就起来编写制定具体的测试用例代码了,上面会指向每一种层面给出代码片段和分析。那么我们先从
actions 开始吧。

为使文章尽量简单、清晰,上边包车型大巴代码片段不是各种文件的全部内容,完整内容在
这里 。

多组件测试

澳门葡京 5

BDD

所谓的BDD行为使得开发,即Behaviour Driven
Development,是一种新的短平快开发方法。它更趋向于须要,须求一块利益者的参与,强调用户故事(User
Story)和行为。二零零六年,在London公布的“敏捷规格,BDD和顶峰测试调换”中,Dan
North对BDD给出了如下概念:

BDD是第3代的、由外及内的、基于拉(pull)的、多方利益相关者的(stakeholder)、各类可扩展的、高自动化的飞跃方法。它讲述了三个互动循环,能够具有带有优良定义的出口(即工作中提交的结果):已测试过的软件。

它对TDD的视角实行了扩展,在TDD中侧重点偏向开发,通过测试用例来规范约束开发者编写出质量更高、bug更少的代码。而BDD特别尊重设计,其要求在安顿测试用例的时候对系统进行定义,倡导使用通用的言语将系统的行事描述出来,将系统规划和测试用例结合起来,从而以此为驱动进行付出工作。

约莫进度:

  1. 从工作的角度定义具体的,以及可衡量的靶子

  2. 找到一种可以高达设定目的的、对工作最重点的那二个成效的措施

  3. 下一场像有趣的事一样描述出三个个现实可实施的作为。其描述方法基于一些通用词汇,那些语汇具有标准科学的表明能力和同等的意思。例如,expect,
    should, assert

  4. 查找适合语言及办法,对行为进行落到实处

  5. 测试人员检验产品运转结果是或不是符合预期行为。最大程度的交给出符合用户愿意的制品,幸免说明不均等带来的题材

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 测试,大家珍视是印证发生的 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);
    });
 
    // …
});

其一测试用例的逻辑很简单,首先创设三个大家盼望的结果,然后调用业务代码,最终证实工作代码的运维结果与梦想是不是同样。这正是写测试用例的中坚套路。

小编们在写测试用例时尽量保持用例的十足职责,不要覆盖太多不一样的业务范围。测试用例数量能够有许七个,但每一个都不应有很复杂。

至于财富加载的挑三拣四

React 即便是个
library,不过它的生态圈十二分的丰裕,会有不行多的可增添框架或类库能够参预使用,然则千万别太快的投入那几个扩大方案。并且每便新加盟二个模块的时候,要在集团内部肯定各类人都以知情了然的。尤其是对于
Redux 自个儿的部分生态扩充,会有很是多的有个别小模块,比如下边那些:

  • 在大家还没起来写 action creatorsreducers 此前,就毫无添加
    redux-actions
  • 在豪门还没写出第13个自身的 form 表单和表单验证的时候,就毫无参与
    redux-form
  • 在大家还没伊始写自身的 selectors 从前,就毫无进入
    reselect
  • 在豪门还么伊始写第贰个高阶组件 HOC 以前,就毫无参预
    recompose
  • …..

同时,关心一些大牛的特级实践,并且创造你协调的一级实践。可是得保险协会中此外小伙伴也能明白。定义清晰的命名规则和目录结构,并且在档次做一些进步的时候得把这一个约定提前探讨清楚。

正文将按如下顺序实行求证:

覆盖率

何以衡量测试脚本的身分呢?当中3个参考指标正是代码覆盖率(coverage)。

什么是代码覆盖率?简单的讲正是测试中运转到的代码占全部代码的比值。当中又能够分为行数覆盖率,分支覆盖率等。具体的意思不再细说,有趣味的可以自动查阅资料。

尽管并不是说代码覆盖率越高,测试的脚本写得越好,然而代码覆盖率对创作测试脚本依然有肯定的指点意义的。

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,大家首要测试多少个地点:

  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 的部分。

保持不断的技艺学习热情

  • 珍视入微技术社区的新势头,比如在接纳中选拔
    ramda.js.,看看怎么在React中优雅的写代码
  • 学学如何行使 React Native
    营造你的运动采纳
  • 学习选用 Electron
    构建你的桌面应用
  • 大概你能够关怀如何行使 Mobx
    来展开利用状态管理
  • React 仅仅只是 UI 层的三个 library
    ,你能够应用澳门葡京,PREACT 和
    inferno 等相近 React
    的库来代替,他们的体积能加轻量,渲染特别便捷,可能是个科学的挑选。
  • Airbnb 的 React/JSX 规范
    小编也建议您抽时间看看,对于协会一致化开发格外有协理。同时,也足以采用ESLint 来开展代码规则检查。
  • I. 单元测试简介
  • II. React 单元测试中用到的工具
  • III. 用测试驱动 React 组件重构
  • IV. React 单元测试常见案例

前者单测工具栈

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瓦斯部分。

结束语

全文实现,谢谢你的阅读,希望一切类别的稿子对你未来的求学抱有协理。

假使您想系统学习 React + Redux
技术栈的装有内容,请点作者前往

2 赞 1 收藏
评论

澳门葡京 6

单元测试(unit testing),是指对软件中的最小可测试单元进行检讨和验证。

测试框架

驷不及舌提供了清晰简明的语法来叙述测试用例,以及对测试用例分组,测试框架会抓取到代码抛出的AssertionError,并追加一大堆附加音信,比如非凡用例挂了,为何挂等等。近期可比盛行的测试框架有:

  • Jasmine:
    自带断言(assert),mock功用
  • Mocha:
    框架不带断言和mock作用,须求组合别的工具,由tj大神开发
  • Jest:
    由推特(Twitter)出品的测试框架,在Jasmine测试框架上衍生和变化开发而来

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(‘模拟发生十一分’)).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 的原由:强大并且有利于测试。

粗略来说,单元正是人为规定的微乎其微的被测作用模块。单元测试是在软件开发进度中要举办的最低级其余测试活动,软件的独门单元将在与程序的其他部分相隔断的场合下进展测试。

断言库

断言库提供了过多语义化的艺术来对值做各样种种的判断。

  • chai:
    如今可比流行的断言库,支持TDD(assert),BDD(expect、should)三种风格
  • should.js:也是tj大神所写

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 相关的了。

测试框架

测试框架的机能是提供部分方便人民群众的语法来描述测试用例,以及对用例举行分组。

mock库

  • sinon.js:使用Sinon,大家得以把其他JavaScript函数替换到三个测试替身。通过安顿,测试替身能够形成各样各类的职责来让测试复杂代码变得不难。接济spies, stub, fake XMLHttpRequest, Fake server, Fake time,很有力

容器组件

容器组件的机要目标是传递 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));
    });
});

很简单有木有,所以也没啥可说的了。

断言(assertions)

预见是单元测试框架中挑顺德的部分,断言战败会招致测试不经过,或报告错误音信。

对于普遍的预知,举一些事例如下:

  • 同等性断言 Equality Asserts

    • expect.toEqual
    • expect.not.toEqual
  • 正如性断言 Comparison Asserts

    • expect.toBeGreaterThan
    • expect.toBeLessThanOrEqual
  • 类型性断言 Type Asserts

    • expect.toBeInstanceOf
  • 条件性测试 Condition Test

    • expect.toBeTruthy()
    • expect.toBeFalsy()
    • expect.toBeDefined()

测试集成管理工科具

  • karma:GoogleAngular
    团队写的,功能很强劲,有诸多插件。能够连绵不断真实的浏览器跑测试。能够用部分测试覆盖率总结的工具总计一下覆盖率;或是能够参预持续集成,提交代码后自行跑测试用例。

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 ,测试用例依然维持不难的韵律。

断言库

断言库首要提供上述断言的语义化方法,用于对出席测试的值做各个种种的论断。这几个语义化方法会重回测试的结果,要么成功、要么战败。常见的断言库有
Should.js, Chai.js 等。

测试脚本的写法

普通,测试脚本与所要测试的源码脚本同名,不过后缀名为.test.js(表示测试)或许.spec.js(表示原则)。

// add.test.js
var add = require('./add.js');
var expect = require('chai').expect;

describe('加法函数的测试', function() {
  it('1 加 1 应该等于 2', function() {
    expect(add(1, 1)).to.be.equal(2);
  });
});

地方那段代码,就是测试脚本,它可以独自执行。测试脚本里面应该包罗2个或四个describe块,每一个describe块应该包括一个或三个it块。

describe块称为”测试套件”(test
suite),表示一组有关的测试。它是三个函数,第3个参数是测试套件的称号(”加法函数的测试”),第二个参数是贰个事实上执行的函数。

describe干的事情正是给测试用例分组。为了尽量多的遮盖种种意况,测试用例往往会有为数不少。那时候通过分组就能够比较便于的保管(那里提一句,describe是足以嵌套的,也正是说外层分组了将来,内部仍是能够分子组)。此外还有一个尤其首要的特色,正是各类分组都能够展开预处理(before、beforeEach)和后处理(after,
afterEach)。

it块称为”测试用例”(test
case),表示二个独自的测试,是测试的小不点儿单位。它也是3个函数,第②个参数是测试用例的称号(”1
加 1 应该对等 2″),第①个参数是三个事实上施行的函数。

大型项目有很多测试用例。有时,大家意在只运转在那之中的多少个,那时能够用only方法。describe块和it块都允许调用only方法,表示只运转有个别测试套件或测试用例。其它,还有skip方法,表示跳过钦点的测试套件或测试用例。

describe.only('something', function() {
  // 只会跑包在里面的测试
})

it.only('do do', () => {
  // 只会跑这一个测试
})

总结

以上正是其一场合完整的测试用例编写思路和演示代码,文中提及的笔触措施也完全能够用在
VueAngular 项目上。完整的代码内容在
这里
(首要的业务多说五次,各位童鞋觉得好帮扶去给个 哈)。

说到底我们得以采纳覆盖率来看下用例的掩盖程度是或不是充足(一般的话不要刻意追求
百分百,依据实际情况来定):
澳门葡京 7

单元测试是 TDD
测试驱动开发的底蕴。从以上全部经过能够阅览,好的安顿分层是很简单编写测试用例的,单元测试不单单只是为着确认保障代码品质:他会逼着您想想代码设计的客观,拒绝面条代码

借用 Clean Code 的结束语:

2006 年,在参与于圣萨尔瓦多进行的高速大会时,埃利sabeth Hedrickson
递给本身一条看似 Lance Armstrong热销的那种茶褐腕带。那条腕带地点写着“沉迷测试”(Test
Obsessed)的字样。小编乐意地戴上,并自豪地一贯系着。自从 1997 年从 KentBeck 这儿学到 TDD 以来,作者的确迷上了测试驱动开发。

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

故而它还在自家的手腕上。在写代码时,笔者用余光瞟见它。它直接提醒笔者,笔者做了写出清新代码的答应。

1 赞 1 收藏
评论

澳门葡京 8

测试用例 test case

为有些特殊目的而编辑的一组测试输入、执行尺度以及预期结果,以便测试有个别程序路径或核实是还是不是满足有些特定必要。

一般的样式为:

it('should ...', function() { ... expect.toEqual;

react 单测示例一

react 单元测试框架
demo1

该框架选择 karma + mocha + chai + sinon 的结合,
是一种采纳工具较多,同时自由度较高的缓解方案。尽管工具库使用的较多,但推动掌握各样工具库的效能和使用,也推进强化对前者单元测试的精通。

当中React的测试库使用
enzyme,React测试必须采取官方的测试工具库,然则它用起来不够便利,所以有人做了包装,推出了一部分第壹方库,当中Airbnb集团的Enzyme最不难上手。

关于该库的 api 使用可参照:

官方文书档案

阮先生React测试入门

测试套件 test suite

平常把一组有关的测试称为3个测试套件

一般的花样为:

describe('test ...', function() { it('should ...', function() { ... }); it('should ...', function() { ... }); ...});

react 单测示例二

react 单元测试框架
demo2

该框架只使用了Jest,是相比较简单的方案,同样也运用了 enzyme。

Jest
是推特开发的1个测试框架,它集成了测试执行器、断言库、spy、mock、snapshot和测试覆盖率报告等功效。React项目本人也是应用Jest进行单测的,由此它们俩的契合度万分高。
前面仅在其里面使用,后开源,并且是在Jasmine测试框架上衍变开发而来,使用了熟谙的expect(value).toBe(other)那种断言格式。

PS: 近年来 enzyme 使用时索要参加设置如下:

import Enzyme from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';

Enzyme.configure({ adapter: new Adapter() });

下边多少个框架方案中都有参预该配置的方式,详见示例。

spy

正如 spy 字面的意味同样,大家用那种“间谍”来“监视”函数的调用情况

通过对监视的函数进行打包,能够经过它驾驭的敞亮该函数被调用过五遍、传入什么参数、重返什么结果,甚至是抛出的非凡情况。

var spy = sinon.spy(MyComp.prototype, 'componentDidMount');...expect(spy.callCount).toEqual;

参考

  • 聊一聊前端自动化测试
  • 前者自动化单元测试初探
  • Javascript的Unit
    Test
  • 单元测试all in
    one
  • Sinon指南: 使用Mocks, Spies 和
    Stubs编写JavaScript测试
  • 测试框架 Mocha
    实例教程

stub

有时会动用stub来放置或许直接调换掉一部分代码,来实现隔断的指标

一个stub能够接纳最少的正视性方法来效仿该单元测试。比如三个措施可能凭借另一个措施的施行,而后人对大家的话是晶莹的。好的做法是选取stub
对它进行隔断替换。那样就实现了更精确的单元测试。

var myObj = { prop: function() { return 'foo'; }};sinon.stub(myObj, 'prop').callsFake(function() { return 'bar';});myObj.prop(); // 'bar'

mock

mock貌似指在测试进度中,对于一些不易于构造可能不易于得到的目的,用一个虚拟的指标来创建以便测试的测试方法

广义的讲,以上的 spy 和 stub 等,以及部分对模块的模仿,对 ajax
重临值的效仿、对 timer 的效仿,都叫做 mock 。

测试覆盖率(code coverage)

用于总括测试用例对代码的测试景况,生成对应的报表,比如 istanbul
是常见的测试覆盖率计算工具

Jest

澳门葡京 9

分歧于”守旧的”的 jasmine / Mocha / Chai 等前端测试框架 —
Jest的利用更简便,并且提供了更高的集成度、更增进的职能。

Jest 是 推特(TWTXC60.US)出品的二个测试框架,相对其余测试框架,其一大特征就是正是停放了常用的测试工具,比如自带断言、测试覆盖率工具,完毕了开箱即用。

除此以外, Jest
的测试用例是并行执行的,而且只进行发生转移的公文所对应的测试,升高了测试速度。

编纂单元测试的语法经常格外简单;对于jest来说,由于其内部使用了
Jasmine 2 来举办测试,故其用例语法与 Jasmine 相同。

事实上,只要先记那住七个单词,就能够应付大多数测试情形了:

  • describe: 定义三个测试套件
  • it:定义两个测试用例
  • expect:断言的测量圭表
  • toEqual:断言的比较结实

describe('test ...', function() { it('should ...', function() { expect.toEqual; expect(sth.length).toEqual; expect(sth > oth).toEqual;});

Jest 号称本身是二个 “Zero configuration testing platform”,只需在
npm scripts在那之中配备了test: jest,即可运维npm test,自动识别并测试符合其规则的(一般是
__test__ 目录下的)用例文件。

骨子里使用中,适当的自定义配置一下,会拿走更切合我们的测试场景:

//jest.config.jsmodule.exports = { modulePaths: [ "<rootDir>/src/" ], moduleNameMapper: { "\.$": '<rootDir>/__test__/NullModule.js' }, collectCoverage: true, coverageDirectory: "<rootDir>/src/", coveragePathIgnorePatterns: [ "<rootDir>/__test__/" ], coverageReporters: ["text"],};

在那几个差不多的安插文件中,我们钦赐了测试的“根目录”,配置了覆盖率(内置的istanbul)的部分格式,并将原来在webpack中对体制文件的引用指向了3个航空模型块,从而跳过了这一对测试无伤大雅的环节

//NullModule.jsmodule.exports = {};

此外值得一提的是,由于jest.config.js是贰个会在npm本子中被调用的平时JS 文件,而非XXX.json.XXXrc的款型,所以 nodejs
的分级操作都能够展开,比如引入 fs
进行预处理读写等,灵活性情外高,能够很好的突出各样门类

是因为是面向src目录下测试其React代码,并且还选取了ES6语法,所以项目下必要存在1个.babelrc文件:

{ "presets": ["env", "react"]}

上述是着力的布署,而其实是因为webpack能够编写翻译es6的模块,一般将babel中设为{ "modules": false },此时的配置为:

//package.json"scripts": { "test": "cross-env NODE_ENV=test jest",},

//.babelrc{ "presets": [ ["es2015", {"modules": false}], "stage-1", "react" ], "plugins": [ "transform-decorators-legacy", 如果对软件测试、接口测试、自动化测试、性能测试、LR脚本开发、面试经验交流。 "react-hot-loader/babel" 感兴趣可以175317069,群内会有不定期的发放免费的资料链接,这些资料都是从各 ], 个技术网站搜集、整理出来的,如果你有好的学习资料可以私聊发我,我会注明出处 "env": { 之后分享给大家。 "test": { "presets": [ "es2015", "stage-1", "react" ], "plugins": [ "transform-decorators-legacy", "react-hot-loader/babel" ] } }}

Enzyme

Enzyme 来自于生动活泼在 JavaScript 开源社区的 Airbnb
公司,是对合法测试工具库(react-addons-test-utils)的包装。

本条单词的London读音为 ['enzaɪm],酵素或酶的情致,Airbnb
并没有给它设计二个图标,估算即便想取用它来解释 React 组件的趣味啊。

它模拟了 jQuery 的
API,相当直观并且易于使用和读书,提供了有的优秀的接口和多少个形式来压缩测试的旗帜代码,方便判断、操纵和遍历
React Components 的输出,并且收缩了测试代码和促成代码之间的耦合。

相似选用 Enzyme 中的 mountshallow 方法,将对象组件转化为一个
ReactWrapper对象,并在测试中调用其各样法子:

import Enzyme,{ mount } from 'enzyme';...describe('test ...', function() { it('should ...', function() { wrapper = mount( <MyComp isDisabled={true} /> ); expect( wrapper.find.exists.toBeTruthy;

sinon

澳门葡京 10

图中这位“小编牵着马”的并不是卷帘老将沙僧…其实图中的传说正是人所皆知的“特罗伊木马”;大致意思正是希腊(Ελλάδα)人围住了特洛伊人十多年,久攻不下,心生一计,把营盘都撤了,只留下2个高大的木马,以及那位被扒光还被打得够呛的人,也等于这里要谈的中坚sinon,由他欺诈特罗伊人
— 前面的旧事剧情大家就都熟知了。

故此那些命名的测试工具呢,也正是各样伪装渗透方法的合集,为单元测试提供了独自而增加的
spy, stub 和 mock 方法,兼容各个测试框架。

即便 Jest 自个儿也有一部分兑现 spy 等的手腕,但 sinon 使用起来特别有利于。

那边不展开琢磨经典的 “测试驱动开发”(TDD – test driven development) 理论

简不难单的说,把测试正向加诸开发,先写用例再稳步落到实处,正是TDD,那是很好掌握的。

而当大家反过头来,对既有代码补充测试用例,使其测试覆盖率不断进步,并在此进程中改革原有设计,修复潜在难点,同时又确定保证原有接口不收影响,那种
TDD 行为固然没人称之为“测试驱动重构”(test driven
refactoring),但“重构”这一个定义本人就包罗了用测试保驾保护航行的情趣,是必需的题中之意。

对于部分零件和共有函数等,完善的测试也是一种最好的使用表明书。

失败-编码-通过 三部曲

鉴于测试结果中,成功的用例会用驼色代表,而未果的部分会展现为大青,所以单元测试也不时被称为
“Red/格林 Testing” 或 “Red/格林 Refactoring” , 那也是 TDD
中的一般性步骤:

  1. 增进2个测试
  2. 运营具有测试,看看新加的那些是还是不是没戏了;要是能学有所成则再度步骤1
  3. 据说退步报错,有针对的编写制定或改写代码;这一步的唯一目的就是经过测试,先不要纠结细节
  4. 重国民党的新生活运动行测试;借使能不负众望则跳到步骤5,否则重复步骤3
  5. 重构已经通过测试的代码,使其更可读、更易维护,且不影响通过测试
  6. 双重步骤1

澳门葡京 11澳门葡京 12

倘诺对软件测试、接口测试、自动化测试、品质测试、LHighlander脚本开发、面试经验调换。感兴趣能够175317069,群内会有不定期的发给免费的素材链接,那几个资料都以从各类技术网站采访、整理出来的,假若您有好的求学材质能够私聊发笔者,作者会评释出处之后享受给大家。

解读测试覆盖率

澳门葡京 13

这就是 jest 内置的 istanbul 输出的覆盖率结果。

故而称之为“伊Stan布尔”,是因为土耳其共和国(Türkiye Cumhuriyeti)地毯世界知名,而地毯是用来”覆盖”的臘‍♀️。

报表中的第贰列至第④列,分别对应四个度量维度:

  • 言辞覆盖率(statement coverage):是还是不是种种语句都推行了
  • 分层覆盖率(branch coverage):是还是不是每种if代码块都实施了
  • 函数覆盖率(function coverage):是不是各类函数都调用了
  • 行覆盖率(line coverage):是不是每一行都实施了

测试结果根据覆盖率被分为“黑褐、深橙、玛瑙红”二种,应该视具体情形尽量进步相应模块的测试覆盖率。

优化重视 让 React 组件变得 testable

理所当然编排组件化的
React,并将丰富独立、作用专一的零件作为测试的单元,将使得单元测试变得简单;

反之,测试的经过让我们更易厘清关系,将原本的组件重构或分解成更客观的布局。分离出的子组件往往也更易于写成stateless的无状态组件,使得质量和关怀点越发优化。

明明钦点 PropTypes

对于部分事先定义并不清晰的零件,能够统一引入
prop-types,鲜明组件可选取的props;一方面能够在支付/编写翻译进度中随时发现错误,其它也得以在协会中任何成员引用组件时形成3个清楚的列表。

用例的预处理或后甩卖

可以用beforeEachafterEach做一些集合的预置和善后工作,在每种用例的前边和后来都会活动调用:

describe('test components/Comp', function() { let wrapper; let spy; beforeEach(function() { jest.useFakeTimers(); spy = sinon.spy(Comp.prototype, 'componentDidMount'); }); afterEach(function() { jest.useRealTimers(); wrapper && wrapper.unmount(); didMountSpy.restore(); didMountSpy = null; }); it('应该正确显示基本结构', function() { wrapper = mount( <Comp ... /> ); expect(wrapper.find.text.toEqual; }); ...});

调用组件的“私有”方法

对于一些零部件中,假设希望在测试阶段调用到其部分中间方法,又不想对原组件改动过大的,能够用instance()赢得组件类实例:

it('应该正确获取组件类实例', function() { var wrapper = mount( <MultiSelect name="HELLOKITTY" placeholder="select sth..." /> ); var wi = wrapper.instance(); expect( wi.props.name ).toEqual( "HELLOKITTY" ); expect( wi.state.open ).toEqual;});

异步操作的测试

作为UI组件,React组件中部分操作供给延时进行,诸如onscrolloninput那类高频触发动作,需求做函数防抖或节流,比如常用的
lodash 的 debounce 等。

所谓的异步操作,在不考虑和 ajax
整合的集成测试的动静下,一般都以指此类操作,只用 setTimeout
是丰硕的,必要搭配 done 函数使用:

//组件中const Comp = =>( <input type="text" onChange={ debounce(props.onSearch, 500) } />);

//单元测试中it('应该在输入时触发回调', function { var spy = jest.fn(); var wrapper = mount( <Comp onChange={ spy } /> ); wrapper.find('#searchIpt').simulate; setTimeout=>{ expect.toHaveBeenCalledTimes; done(); }, 550);});

有的大局和单例的模仿

一部分模块中也许耦合了对 window.xxx
这类全局对象的引用,而浑然去实例化这些指标可能又牵涉出许多别的的题材,难以开展;此时得以见招拆招,只模拟2个最小化的全局对象,保险测试的展开:

//fakeAppFacade.jsvar facade = { router: { current: function() { return {name:null, params:null}; } }, appData: { symbol: "&yen;" }};window._appFacade = facade;module.exports = facade;

//测试套件中import fakeFak from '../fakeAppFacade';

其它比如 LocalStroage
那类对象,测试端环境中绝非原生协理,也足以简不难单模拟一下:

//fakeStorage.jsvar _util = {};var fakeStorage = { "set": function { _util['_fakeSave_'+k] = v; }, "get": function { return _util['_fakeSave_'+k] || null; }, "remove": function { delete _util['_fakeSave_'+k]; }, "has": function { return _util.hasOwnProperty('_fakeSave_'+k); }};module.exports = fakeStorage;

棘手的 react-bootstrap/modal

在四个档次中用到了 react-bootstrap
界面库,测试二个零件时,由于包蕴了其 Modal
模态弹窗,而弹窗组件是暗中同意渲染到 document 中的,导致难以用普通的
find 方法等收获

解决的办法是模拟二个渲染到容器组件原处的平时组件:

//FakeReactBootstrapModal.jsimport React, {Component} from 'react';class FakeReactBootstrapModal extends Component { constructor { super; } render() { //原生的 react-bootstrap/Modal 无法被 enzyme 测试 const { show, bgSize, dialogClassName, children } = this.props; return show ? <div className={ `fakeModal ${bgSize} ${dialogClassName}` }>{children}</div> : null; }}export default FakeReactBootstrapModal;

并且在组件渲染时,插足判断逻辑,使之能够支撑自定义的类代替 Modal 类:

//ModalComp.jsimport { Modal } from 'react-bootstrap';...render() { const MyModal = this._modalClass || Modal; return (<MyModal bsSize={props.mode>1 ? "large" : "middle"} dialogClassName="custom-modal"> ... </MyModal>;}

而测试套件中,达成八个测试专用的子类:

//myModal.spec.jsimport ModalComp from 'components/ModalComp';class TestModalComp extends ModalComp { constructor { super; this._modalClass = FakeReactBootstrapModal; }}

如此测试即可顺遂举办,跳过了并不主要的 UI 效果,而各样逻辑都能被掩盖了

模拟fetch请求

假定对软件测试、接口测试、自动化测试、质量测试、LCR-V脚本开发、面试经验交换。感兴趣能够175317069,群内会有不定期的发给免费的素材链接,那个素材都以从各样技术网站采访、整理出来的,假使您有好的就学材质能够私聊发作者,笔者会注解出处之后享受给大家。

在单元测试的进程中,难免境遇一些必要长途请求数据的事态,比如组件获取伊始化数据、提交变化数据等。

要小心那种测试的目标只怕考察组件自己的展现,而非重点关怀实际远程数据的合龙测试,所以大家无需真实的请求,能够省略的效仿一些请求的现象。

sinon 中有一部分仿照 XMLHttpRequest 请求的法门, jest
也有一些第叁方的库化解 fetch 的测试;

在大家的花色中,依据实际的用法,自个儿达成二个类来模拟请求的响应:

//FakeFetch.jsimport { noop } from 'lodash';const fakeFetch = (jsonResult, isSuccess=true, callback=noop)=>{ const blob = new Blob( [JSON.stringify(jsonResult)], {type : 'application/json'} ); return =>{ console.log('FAKE FETCH', args); callback.call(null, args); return isSuccess ? Promise.resolve( new Response( blob, {status:200, statusText:"OK"} ) ) : Promise.reject( new Response( blob, {status:400, statusText:"Bad Request"} ) ) }};export default fakeFetch;

//Comp.spec.jsimport fakeFetch from '../FakeFetch';const _fc = window.fetch; //缓存“真实的”fetchdescribe('test components/Comp', function() { let wrapper; afterEach(function() { wrapper && wrapper.unmount(); window.fetch = _fc; //恢复 }); it("应该在远程请求时响应onRemoteData", =>{ window.fetch = fakeFetch({ brand: "GoBelieve", tree: { node: '总部', children: null } }); let spy = jest.fn(); wrapper = mount( <Comp onRemoteData={ spy } /> ); jest.useRealTimers(); _clickTrigger(); //此时应该发起请求 setTimeout=>{ expect(wrapper.html.toMatch; expect.toHaveBeenCalledTimes; done(); }, 500); });}); 

单元测试作为一种经典的付出和重构手段,在软件开发领域被周边承认和使用;前端领域也逐步积淀起了丰盛的测试框架和方法。

单元测试能够为我们的支出和护卫提供基础保险,使我们在思绪清晰、心中有底的意况下成功对代码的搭建和重构;

内需注意的是,世上没有包治百病的良药,单元测试也决不是万金油,秉持谨慎认真负责的千姿百态才能从根本上有限援助大家办事的拓展。

相关文章

发表评论

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

*
*
Website