zoukankan      html  css  js  c++  java
  • react前端自动化测试: jest + enzyme

    1.背景


    本文中的自动化测试指的是单元测试 (UT),所谓单元测试也就是对每个单元进行测试,通俗的将一般针对的是函数,类或单个组件,不涉及系统和集成。单元测试是软件测试的基础测试,主要是用来验证所测代码是否和程序员的期望一致。

    jest 是 facebook 开源的,用来进行单元测试的框架,功能比较全面,测试、断言、覆盖率它都可以,另外还提供了快照功能。

    2.安装与配置


     2.1安装

    安装jest

    npm install --save-dev jest 

    安装babel-jest

    npm install --save-dev babel-jest

    安装enzyme,需要根据项目的react版本来安装对应的enzyme

    npm install --save-dev enzyme enzyme-adapter-react-16

    安装react-test-renderer

    npm install --save-dev react-test-renderer

     2.2配置

    package.json中添加:

    {
      "scripts": {
        "test": "jest"
      }
    }

    执行npm run test 命令可在终端运行查看测试运行结果。

    同时 Jest 还提供了生成测试覆盖率报告的命令,只需要添加上 --coverage 这个参数既可生成,再加上--colors可根据覆盖率生成不同颜色的报告(<50%红色,50%~80%黄色, ≥80%绿色)

    "test": "jest --colors --coverage",

    .babelrc文件中添加,请根据自己的项目情况调整

    {
    "env": {
        "test": {
          "presets": [["next/babel", { "preset-env": { "modules": "commonjs" }, "styled-jsx": {
            "plugins": [
              "styled-jsx-plugin-postcss"
            ]
          } }]]
        }
      }
    }

    jest.config.js: jest配置文件,可放在根目录下或config文件下(也可以起其他名字或者直接写在package.json里)                                

    module.exports = {
      setupFiles: ['<rootDir>/jest.setup.js'], // 运行测试前可执行的脚本(比如注册enzyme的兼容)
      transform: {
        '^.+\.(js|jsx|mjs)$': '<rootDir>/node_modules/babel-jest',
        '^.+\.css$': '<rootDir>/__test__/css-transform.js',
      },
      testPathIgnorePatterns: ['<rootDir>/.next/', '<rootDir>/node_modules/'], //转换时需忽略的文件
      testURL: 'http://localhost/', // 运行环境下的URl
    };

    还有一些配置, 详细的配置见jest官网

      collectCoverage: true, // 是否收集测试时的覆盖率信息(默认是false,同package配置的--coverage参数)
      collectCoverageFrom: ['<rootDir>/src/**/*.{js,jsx,mjs}'], // 哪些文件需要收集覆盖率信息
      coverageDirectory: '<rootDir>/test/coverage', // 输出覆盖信息文件的目录
      coveragePathIgnorePatterns: ['/node_modules/', '<rootDir>/src/index.jsx'], // 统计覆盖信息时需要忽略的文件
      moduleNameMapper: { // 需要mock处理掉的文件,比如样式文件 },
      testMatch: [ // 匹配的测试文件
        '<rootDir>/test/**/?(*.)(spec|test).{js,jsx,mjs}',
        '<rootDir>/src/**/__tests__/**/*.{js,jsx,mjs}',
      ],

    jest.setup.js

    /* eslint-disable import/no-extraneous-dependencies */
    import { configure } from 'enzyme';
    import Adapter from 'enzyme-adapter-react-16';
    
    configure({ adapter: new Adapter() });

    3.测试


     通常测试文件名与要测试的文件名相同,后缀为.test.js,所有测试文件默认放在__test__文件夹中。

    describe块之中,提供测试用例的四个函数:before()、after()、beforeEach()和afterEach()。它们会在指定时间执行(如果不需要可以不写)

    describe('加法函数测试', () => {
    
      before(() => {// 在本区块的所有测试用例之前执行
      });
    
      after(() => {// 在本区块的所有测试用例之后执行
      });
    
      beforeEach(() => {// 在本区块的每个测试用例之前执行
      });
    
      afterEach(() => {// 在本区块的每个测试用例之后执行
      });

    it('1加1应该等于2', () => { expect(add(1, 1)).toBe(2); }); it('2加2应该等于4', () => { expect(add(2, 2)).toBe(42); });
    });

    测试文件中应包括一个或多个describe, 每个describe中可以有一个或多个it,每个describe中可以有一个或多个expect.

    describe称为"测试套件"(test suite),it块称为"测试用例"(test case)。

    expect就是判断源码的实际执行结果与预期结果是否一致,如果不一致就抛出一个错误.

     3.1简单测试

    import React from 'react';
    
    export default () => (
      <div>404</div>
    );
    /* eslint-env jest */
    import { shallow } from 'enzyme';
    import React from 'react';
    import Page404 from '../components/Page404';
    
    describe('Page404', () => {
      it('Page404 shows "404"', () => {
        const app = shallow(<Page404 />);
        expect(app.find('div').text()).toEqual('404');
      });
    });

    这个测试只测试了组件是否被正常显示出来了。expect部分是断言,实现内容是在被渲染出的Page404组件中找到div标签,然后断言它的text()中有没有包含期望的文字。通过这种方式我们可以得知组件是否有被显示出来。

    除了text()属性以外,还可非常灵活的通过其他方式来得知组件是否被正常显示。例如:

    expect(wrapper.find('.card').exists()).toBeTruthy()
    expect(wrapper.find('input').props().type).toBe('text')

     npm test运行所有测试文件或 npm test <name> 运行匹配的测试文件:

    • % Stmts是语句覆盖率(statement coverage):是否每个语句都执行了

    • % Branch分支覆盖率(branch coverage):是否每个分支代码块都执行了(if, ||, ? : )

    • % Funcs函数覆盖率(function coverage):是否每个函数都调用了

    • % Lines行覆盖率(line coverage):是否每一行都执行了

    在这里简单介绍下enzyme

    enzyme是Airbnb开源的react测试类库,提供了一套简洁强大的API,并通过jquery风格的方式进行dom处理,开发体验十分友好. 它提供三种测试方法

    shallow:

    shallow 返回组件的浅渲染,对官方shallow rendering 进行封装。浅渲染 作用就是:它仅仅会渲染至虚拟dom,不会返回真实的dom节点,这个对测试性能有极大的提升。shallow只渲染当前组件,只能能对当前组件做断言

     mount :

    mount 方法用于将React组件加载为真实DOM节点。mount会渲染当前组件以及所有子组件

    render:

    render 采用的是第三方库Cheerio的渲染,渲染结果是普通的html结构,对于snapshot使用render比较合适。

    多数情况下,shallow 方法就能满足我们的需求了。

    Enzyme的一部分API,你可以从中了解它的大概用法。详细的API

    .get(index):返回指定位置的子组件的DOM节点

    .at(index):返回指定位置的子组件

    .first():返回第一个子组件

    .last():返回最后一个子组件

    .type():返回当前组件的类型

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

    .html():返回当前组件的HTML代码形式

    .props():返回根组件的所有属性

    .prop(key):返回根组件的指定属性

    .state([key]):返回根组件的状态

    .setState(nextState):设置根组件的状态

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

     例如:

    expect(wrapper.find('input').prop('value')).toBe('default value');

     3.2 模拟 Props,渲染组件创建 Wrapper

    /* eslint-env jest */
    import { shallow } from 'enzyme';
    import React from 'react';
    import { OrderManage } from '../../components/purchaser/OrderManege';
    
    const setup = ({ ...props }) => {
      const wrapper = shallow(<OrderManage {...props} />);
      return {
        props,
        wrapper,
      };
    };
    describe(
    'OrderManage', () => { it('role is operator', () => { const { wrapper } = setup({ role: 'operator', isFetching: true, fetchOrdersByStatuses: () => {}, // 直接设为空函数
        getData: jest.fn(), // Jest 提供的mock 函数 }); const params
    = { node: { id: 2, }, }; expect(wrapper.instance().handlePageChange(1)); expect(wrapper.instance().OrderManagementLink(params)); expect(wrapper.find('.loader')).toHaveLength(1); expect(wrapper.find('.order-simpleGrid')).toHaveLength(0); expect(wrapper.type()).toEqual('div'); }); });

    在正式测试功能之前,我们要写一个 setup方法用来渲染组件,因为每一个测试case都会用到它

    3.3 组件中的方法测试

    export class Card extends React.Component {
      constructor (props) {
        super(props)
    
        this.cardType = 'initCard'
      }
    
      changeCardType (cardType) {
        this.cardType = cardType
      }
      ...
    }
    it('changeCardType', () => {
      let component = shallow(<Card />)
      expect(component.instance().cardType).toBe('initCard')
      component.instance().changeCardType('testCard')
      expect(component.instance().cardType).toBe('testCard')
    })

    其中,instance 方法可以用于获取组件的内部成员对象。

     3.4 模拟事件测试

     <Input value={value} onChange={e => this.handleChange(e)}/>
    it('can save value and cancel', () => {
       const value = 'edit'
       const {wrapper, props} = setup({
          editable: true
       });
       wrapper.find('input').simulate('change', {target: {value}});
       wrapper.setProps({status: 'save'});
       expect(props.onChange).toBeCalledWith(value);
    })

    我们可以在这个返回的 dom 对象上调用类似 jquery 的api进行一些查找操作,还可以调用 setProps 和 setState 来设置 props 和 state,也可以用 simulate 来模拟事件,

    触发事件后,去判断props上特定函数是否被调用,传参是否正确;组件状态是否发生预料之中的修改;某个dom节点是否存在是否符合期望。

    例: 

    wrapper.find('button').simulate('click');

    wrapper.find('input').simulate('keyup');

    expect(props.onClick).toBeCalled();// onClick方法被调用

    expect(props.onClick).not.toBeCalled() // onClick方法没被调用

    3.5 对生命周期的测试

    对于

    • componentWillMount
    • componentWillUpdate
    • componentWillReceiveProps
    • shouldComponentUpdate
    • componentWillUnmount 

    可以使用 Enzyme 中的 shallow 方法加载组件,例如

      /* eslint-env jest */
      import { shallow } from 'enzyme';
     import sinon from 'sinon';
     import { App } from '../App';


    it('componentWillMount', () => { sinon.spy(App.prototype, 'componentWillMount'); shallow(<App />); expect(App.prototype.componentWillMount.calledOnce).toBeTruthy(); });
    it('componentWillReceiveProps', () => {
      let wrapper = shallow(<App role={''} />);
      sinon.spy(App.prototype, 'componentWillReceiveProps')
      wrapper.setProps({
        role: 'admin'
      });
     expect(App.prototype.componentWillReceiveProps.calledOnce).toBeTruthy();
    })

    其中,spy 是 sinon 提供的特殊函数,它可以获取关于函数调用的信息。例如,调用函数的次数、每次调用的参数、返回的值、抛出的错误等,可以用来测试一个函数是否被正确地调用。npm i --dave-dev sinon 安装sinon.

    而对于

    • componentDidMount
    • componentDidUpdate

    要用enzyme的mount方法进行加载。

    3.6 使用snapshot进行UI测试

    import renderer from 'react-test-renderer'
    
    it('App -- snapshot', () => {
       const renderedValue = renderer.create(<App />).toJSON()
       expect(renderedValue).toMatchSnapshot()
    })

     jest的特色, 快照测试第一次运行的时候会将 React 组件在不同情况下的渲染结果(挂载前)保存一份快照文件。后面每次再运行快照测试时,都会和第一次的比较,diff出两次快照的变化。

    如果需要更新快照文件,使用  npm run test -- -u 命令

    3.7 Redux测试

    redux官网有详细的例子,送上传送门

    4.总结


           上面主要介绍了UT的安装配置及几个测试demo,以前没有接触过单元测试,各种踩坑与啃读API(jest + enzyme),这些demo基本可以满足项目中的测试,后续在写测试中再进步。刚开始接触测试是一点思路也没有,看见组件后无从下手,也一直在思考花费这么多时间写测试到底值不值得,下面是目前遇到的问题和一些思考中的问题,可以一起讨论一下:

    1. 一个好测试的标准,覆盖率越高就一定越好吗
    2. 开发前还是开发后测试
    3. 怎么测纯函数的组件(函数中的const之后总是执行不到)
    4. error: TypeError: Only absolute URLs are supported 未解决
  • 相关阅读:
    重温MVC基础入门
    重温ASP.NET WebAPI(一)初阶
    WebApi的安全性及其解决方案
    Asp.net Core + EF Core + Bootstrap搭建的MVC后台通用管理系统模板(跨平台版本)
    ASP.NET经典权限解决方案,适用于OA、CRM、ERP、HR等应用系统
    一款MVC5+EF+Bootstrap搭建的后台通用管理系统模板
    mac设置多个屏幕显示的问题
    JavaScript有这几种测试
    Script error.解决方法
    Script error.深度测试
  • 原文地址:https://www.cnblogs.com/susu8/p/9512393.html
Copyright © 2011-2022 走看看