React单元测试实战

avatar
SugarTurboS Club @SugarTurboS
  • 苏格团队
  • 作者:Dee

前言

单元测试的好处

  • 可保证得到结果的一致性,提高项目、组件稳定性。
  • 开发者按单元测试思路去写代码,可清晰代码结构,提高代码的可读性。

由于笔者开发的项目越来越大,公共组件的复用性高,故其稳定性尤为重要。因此,引入单元测试刻不容缓。

单元测试的不好

  • 会占用一定的开发成本,增加开发工作量。
  • 旧项目加入单元测试改动很大,会有一定的风险。
  • 会有一定的学习成本,对开发者要求比较高。
  • 如果在一些复用性很低的组件使用单元测试,成效不大且开发成本高。

选型

在做项目单元测试前,笔者参考了网上的一些文章以及官方文档,最后选型为Jest + react-test-renderer + Enzyme。

  • Jest

    Jest 是 Facebook 出品的一个测试框架,相对其他测试框架,其一大特点就是就是集成了 Mocha,chai,jsdom,sinon等功能,内置了常用的测试工具,比如自带断言、测试覆盖率工具,实现了开箱即用。

  • react-test-render

    配合react-test-render,Jest 可提供了快照测试功能。

    首次运行快照测试,会产生一个可读的快照,再次测试时会通过比对快照文件和新产生的快照判断测试是否通过。

    Jest在执行的时候如果发现toMatchSnapshot方法,会在同级目录下生成一个__ snapshots__文件夹用来存放快照文件,以后每次测试的时候都会和第一次生成的快照进行比较。

  • Enzyme

    React官方已经提供了一个测试工具库:react-dom/test-utils。但是用起来不够方便,于是有了一些第三方的封装库,比如Airbnb公司的Enzyme。其两大特点:

    • 提供了一套简洁强大的 API,并内置Cheerio
    • 实现了jQuery风格的方式进行DOM 处理,开发体验十分友好

    三种渲染方法

    shallow:浅渲染,是对官方的Shallow Renderer的封装。将组件渲染成虚拟DOM对象,只会渲染第一层,子组件将不会被渲染出来,使得效率非常高。不需要DOM环境, 并可以使用jQuery的方式访问组件的信息

    render:静态渲染,它将React组件渲染成静态的HTML字符串,然后使用Cheerio这个库解析这段字符串,并返回一个Cheerio的实例对象,可以用来分析组件的html结构

    mount:完全渲染,它将组件渲染加载成一个真实的DOM节点,用来测试DOM API的交互和组件的生命周期。用到了jsdom来模拟浏览器环境

    三种方法中,shallowmount因为返回的是DOM对象,可以用simulate进行交互模拟,而render方法不可以。一般shallow方法就可以满足需求,如果需要对子组件进行判断,需要使用render,如果需要测试组件的生命周期,需要使用mount方法。

    注意:enzyme还需要根据React的版本安装适配器,适配器对应表如下:

方案

前面说了这么多,是时候上代码了。

  • 目录

    笔者在根目录新建一个unitTest目录,其目录结构为:

    • jest.config.js:jest配置文件

    • mocks:mock文件目录

    • components:项目的公共组件单元测试用例目录

    • components/__ snapshots __:运行单元测试时自动生成的快照存放目录

  • 安装(由于笔者是react16版本,所以安装的适配器版本为enzyme-adapter-react-16)

    npm install jest enzyme enzyme-adapter-react-16 react-test-renderer

  • 配置

    Jest支持直接在package.json文件写入配置,但笔者有轻微洁癖,喜欢把配置文件写到unitTest里面,方便查找以及阅读。

    // package.json
    {
        "scripts": {
            "jest": "jest --config ./unitTest/jest.config.js", // 单元测试
            "jestupdate": "jest --config ./unitTest/jest.config.js --updateSnapshot" // 单元测试快照更新
            "jestreport": "jest --config ./unitTest/jest.config.js --coverage" // 单元测试并生成覆盖率报告
        }
    }
    
    // jest.config.js
    module.exports = {
        testURL: 'http://localhost/',
        setupFiles: [],
        moduleFileExtensions: ['js', 'jsx'],
        testPathIgnorePatterns: ['/node_modules/'],
        testRegex: '.*\\.test\\.js$',
        collectCoverage: false,
        collectCoverageFrom: ['src/components/**/*.{js}'],
        moduleNameMapper: {
            '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
                '<rootDir>/mocks/fileMock.js',
            '\\.(css|less|scss)$': '<rootDir>/mocks/styleMock.js'
        }
    };
    
    
    • testURL: jsdom运行url,默认为"about:blank",如果不设置,会在尝试访问localStorage出错。
    • setupFiles:运行测试代码前,Jest会先运行setupFile指定的配置文件来初始化测试环境。
    • moduleFileExtensions:支持单元测试的文件扩展名。
    • testPathIgnorePatterns:匹配忽略文件规则。
    • testRegex:匹配测试文件规则。
    • collectCoverage:是否生成测试覆盖报告,开启会增加测试时间。
    • collectCoverageFrom:指示应收集覆盖率信息的一组文件。如果文件与指定的glob模式匹配,即使此文件不存在测试,也将为其收集覆盖率信息,并且测试套件中从不需要它。
    • moduleNameMapper:可用于将模块路径映射到不同的模块。默认情况下,预设将所有图像映射到图像存根模块,但如果找不到模块,可配置此选项。
  • mock文件

    // fileMock.js
    module.exports = {};
    
    // styleMock.js
    module.exports = {};
    
  • 编写单元测试

    // button.test.js
    import Button from '../../src/common/components/Button';
    import renderer from 'react-test-renderer';
    import React from 'react';
    import { shallow, configure } from 'enzyme'; // shallow(浅渲染,只渲染父组件)
    import Adapter from 'enzyme-adapter-react-16'; // 适应React-16
    configure({ adapter: new Adapter() }); // 适应React-16,初始化
    const props = {
        text: '按钮测试用例',
        type: 'white',
        style: { marginTop: 15 },
        size: 'big',
        disabled: false,
        height: 'middle',
        isLock: true,
        cname: 'hello',
        onClick: () => {}
    };
    describe('test Button', () => {
        it('button render correctly', () => {
            const tree = renderer.create(<Button {...props} />).toJSON();// 生成快照
            expect(tree).toMatchSnapshot(); // 匹配之前的快照
        });
    
        it('button has class', () => {
            const item = shallow(<Button {...props} />); //浅渲染
            expect(item.hasClass('hello')).toBe(true); // 断言有item有hello的className
        });
    });
    
    
    

后记

注意事项:

1、如果不配置testURL,会报错:localStorage is not available for opaque origins

2、本文档只讲述笔者的实践方案以供参考,关于Jest、enzyme的具体介绍、用法可参考