单元测试总结

备案系统前端 单元测试总结

背景:

备案系统,逻辑复杂,表单数据繁多,测试不充分,导致线上问题较多。
出现问题较多的大致分类:

  • 测试不充分,边缘情况(线上特殊例子)没考虑到,例如:空值判断、字段校验中的正则表达式覆盖不全面(比如 域名后缀)等
  • 备案类型多,功能相似,逻辑相同点有多处,修改处容易遗漏,例如:不同备案类型的表单都有提交、保存、新旧表单值比较等方法。
  • 关联功能多(比如多处逻辑公用组件),也会存在拆东墙补西墙 等......

规避大部分线上问题思路:

  • 关于 边缘情况的特殊例子通过jest单元测试用例,补充所有测试场景来规避。
  • 业务逻辑中使用ts重构,规范定义 变量/函数类型,以及props中参数类型等。
  • 类型多,功能相似,将表单的提交/保存方法的主要逻辑部分抽取出,写测试用例测试。同时开发过程中,写好注释。

具体方案 ts + jest + enzyme:

  1. 公共函数部分: jest
  2. 公共组件部分: enzyme(部分组件的写法不适合测试,需要优化)
  3. 具体表单逻辑部分使用ts重构,增强类型校验,减少问题的产生
  4. 根据测试报告,来补充测试用例,提高测试场景的覆盖率

执行步骤:

配置测试环境 --------> 锁定测试范围 ------> 模拟测试数据 -----------> 编写测试用例 --------> 核实测试覆盖率

  1. 配置jest必备的执行环境
  • 查找文件的根路径
  • 不转译的模块
  • 模块别名
  • 路径别名
  • 需要babel编译的文件
  • SetUpFile全局变量配置 和 引入enzyme
  • 测试覆盖率配置
  • 生成测试报告
var path = require('path');
var SRC_ROOT = path.join(__dirname, 'src/');
var ROOT = path.join(__dirname, '/');

var conf = {
    // 多于一个测试文件运行时, 是否展示每个测试用例测试通过情况
    verbose: true,
    globals: { CONSOLE_ENV: 'pre' }, 
    rootDir: ROOT,
    // 以 SRC_ROOT 这个目录做为根目录来搜索测试文件(模块)
    roots:[SRC_ROOT],
    modulePaths: ['<rootDir>/src'],
    //node_modules都被忽略, 不需要被转译
    transformIgnorePatterns:['<rootDir>/node_modules/'],
    // 在测试环境准备好之后且每个测试文件执行之前运行下述文件
    setupFilesAfterEnv: ['<rootDir>/src/test/jest.setup.js'],
    transform: {
        "^.+\\.[t|j]sx?$": "babel-jest"
    },
    // 例如,require('uc_components') 会解析成 require('@ucloud/uc-components')
    moduleNameMapper: {
          "uc_components": "@ucloud/uc-components",
          "react_components": "@ucloud-fe/react-components",
          "libs/Yhwach": "<rootDir>/src/lib/Yhwach"
    },
    // 例如,require('./a') 语句会先找 `a.js`,找不到找 `a.jsx`
    moduleFileExtensions: ['js', 'json', 'jsx', 'ts', 'tsx', 'node'],

    // 收集这些文件的测试覆盖率
    collectCoverageFrom: [
        'src/components/**/*.{js,jsx}',
        'src/models/*.{js,jsx}',
        'src/utils/*.{js,jsx}',
        'src/stores/*.{js,jsx}'
    ],
    collectCoverage: true
};

module.exports = conf;
y
  1. mock测试数据,单独定义在mock.js文件中
  2. 设置全局变量jest.setup.js(在测试文件执行之前,执行该文件)
    • 测试引入enzyme包
    • 测试所需要依赖的npm包,挂载到global上
    • 测试所需要的所有mock数据,挂载到global上
    • 定义测试所需的所有异步 api mock数据
import Enzyme, { shallow, render, mount } from 'enzyme';
import Adapter from 'enzyme-adapter-react-15'; // React 15 需要修改对应版本
import React, { PropTypes } from 'react';
import _ from 'underscore';
import config, {
    order,
    website,
    errWebsite,
    order2,
    order3,
    describeICPMainResponse,
    GetAuthCodeListResponse
} from './mock';//测试数据
import 'babel-polyfill'//使用es6+的语法糖
//初始化enzyme
Enzyme.configure({
    adapter: new Adapter()
});
//依赖的模块挂载到全局变量
global.shallow = shallow;
global.render = render;
global.mount = mount;
global.React = React;
global.PropTypes = PropTypes;
global._ = _;
//测试数据挂载到全局变量
global.config = config;
global.order = order
global.website = website
global.errWebsite = errWebsite
global.order2 = order2
global.order3 = order3
global.describeICPMainResponse = describeICPMainResponse
global.GetAuthCodeListResponse = GetAuthCodeListResponse
//模拟api request方法的封装
jest.mock('uc_common_components', () => ({
    getNgService: () => {},
    queryService: {
        query: () => (Promise.resolve())
    }
}), {
    virtual: true
})
//模拟api接口返回值
jest.mock('models/apis', () => ({
    asyncCheckIp: (value) => true,
    describeICPConfig: () => Promise.resolve({
        ...global.config
    }),
    createICPPicture: () => {},
    createICPOrder: () => {},
    describeICPOrder: () => Promise.resolve({
        RetCode: 0,
        ICP: []
    }),
    modifyICPOrder: () => Promise.resolve({
        RetCode: 0
    }),
    describeICPMain: () => Promise.resolve(global.describeICPMainResponse),
    deleteICPOrder: () => {},
    describeICPPicture: () => {},
    applyICPCurtain: () => {},
    DescribeWeb: () => {},
    asyncCheckOrganizerName: () => {},
    checkIsDisabledEdit: () => {},
    ExtractICPType: () => {},
    CheckAuthNameAndPicName: () => {},
    GetAuthCodeList: () => Promise.resolve(global.GetAuthCodeListResponse),
    GetAuthIpList: () => {},
    CreateAuthCode: () => {},
    CheckAuthCode: () => {},
    CheckIsAuth: () => {},
}))
  1. 根据测试报告覆盖情况,完善测试用例

目前写的测试用例:

如下图:分为几个目录,公共函数部分、stores数据部分、components基础组件部分

工具类函数,单元测试例子:
//举个例子,写测试校验身份证号码的测试用例
const { IdentityCodeValid  } = require('utils/validateRule.js');

test('valid identityCode to equal {pass : true}', () => {
  expect(IdentityCodeValid('411325199510037826')).toEqual({"pass": true, "tip": ""});
});
test('valid identityCode to equal {pass : false, tip:"身份证号格式错误"}', () => {
  expect(IdentityCodeValid('4132519951003782X')).toEqual({"pass": false, "tip": "身份证号格式错误"});
});


test('valid identityCode to equal {pass : false, tip: "地址编码错误"}', () => {
  expect(IdentityCodeValid('47132519951003782X')).toEqual({"pass": false, "tip": "地址编码错误"});
});

test('valid identityCode to equal {pass : false, tip: "证件号录入错误"}', () => {
  expect(IdentityCodeValid('41132519951003782X')).toEqual({"pass": false, "tip": "证件号录入错误"});
});
表单校验,单元测试用例
//表单中,该字段可能会出现的所有情况,都列举出来。
test('valid websiteForm Domain is true', async () => {
    try {
        let result = await websiteCheckMap['Domain'](website.Domain, website, order)
        expect(result).toBe(true)
    } catch (e) {
        expect(e).toBe('error')
    }
})
test('valid websiteForm Domain [aaa.com] is 域名格式错误', async () => {
    try {
        let result = await websiteCheckMap['Domain'](['aaa.com'], website, order)
        expect(result).toBe('域名格式错误')
    } catch (e) {
        expect(e).toBe('error')
    }
})
test('valid websiteForm Domain is 网站域名证书不能为空!', async () => {
    try {
        let result = await websiteCheckMap['Domain']([{
            "Domain": "sss22.com",
            "IsMain": 1,
            "key": "main_domain_30"
        }], website, order)
        expect(result).toBe('网站域名证书不能为空!')
    } catch (e) {
        expect(e).toBe('error')
    }
})

test('valid websiteForm Domain is 请输入正确的域名', async () => {
    try {
        let result = await websiteCheckMap['Domain']([{
            "CerificationPicture": ["6e9a7b8d2bc4ae59a588b24de4bf243389323920.jpg"],
            "Domain": "sss22.websites",
            "IsMain": 1,
            "key": "main_domain_30"
        }], website, order)
        expect(result).toBe('请输入正确的域名')
    } catch (e) {
        expect(e).toBe('error')
    }
})

test('valid websiteForm Domain [] is 域名列表不能为空!', async () => {
    try {
        let result = await websiteCheckMap['Domain']([], website, order)
        expect(result).toBe('域名列表不能为空!')
    } catch (e) {
        expect(e).toBe('error')
    }
})

test('valid websiteForm Domain null is 域名列表不能为空!', async () => {
    try {
        let result = await websiteCheckMap['Domain'](null, website, order)
        expect(result).toBe('域名列表不能为空!')
    } catch (e) {
        expect(e).toBe('error')
    }
})
stores数据管理部分
import AuthCodeStore from 'stores/authCode'
//主要是测试,store调用文件中的刷新数据函数时,数据是否是自己预设的模拟数据
test('valid AuthCodeStore refresh return CodeList', async () => {
    try {

        await AuthCodeStore.refresh()
        const authCodeList = AuthCodeStore.get()
        expect(authCodeList).toEqual(
	        //判断返回的数组中,是否包含预设的数据
            expect.arrayContaining(
                [{
                    "Code": "7b7a445a-6649-419c-8d5f-8d79812bd2c8",
                    "TargetEmail": "wei.yu@ucloud.cn",
                    "Status": "未使用"
                }]
            )
        )
		//清空数据
        AuthCodeStore.dispatch([])
        //调用获取数据方法
        const authCodeList2 = AuthCodeStore.get()
        //判断数据此时是否为空数组
        expect(authCodeList2).toEqual([])

    } catch (e) {
        expect(e).toBe('error')
    }
})
组件的校验
import LanguageSelect from 'components/language-select/LanguageSelect'
import renderer from 'react-test-renderer'
import 'jest-styled-components'

describe('test suite: LanguageSelect component', () => {
	//生成组件快照,如果组件被修改,再次执行测试,会报错提醒。
	//1、需要重新生成快照,2、下面的测试用例需要同步更新
    it('case: expect LanguageSelect create a Snapshot', () => {
        const wrapper = renderer.create( 
            <LanguageSelect 
                config={global.config}
                expandedNum={2}
                values = {[]}
                collection='SortedLanguage'
            />
        ).toJSON();
        expect(wrapper).toMatchSnapshot();
    });
    //判断主要渲染的元素,是否缺失
    it('LanguageSelect has children 17', () => {
        let values = []
        const onChange = jest.fn(v=>{
            values = v
        })
        const wrapper = mount(
        <LanguageSelect 
            config={global.config} 
            expandedNum={2}
            values = {values}
            onChange={onChange}
            collection='SortedLanguage'
        />); //完全渲染

        expect(wrapper.find('span.uc-fe-checkbox').length).toEqual(2)

        wrapper.find('a').simulate('click')
        expect(wrapper.find('span.uc-fe-checkbox').length).toEqual(global.config['SortedLanguage'].length)
    });
    //模拟onChange事件
    it('LanguageSelect trigger onChange update values', () => {
        let values = []
        const onChange = jest.fn(v=>{
            values = v
        })
        const wrapper = mount(
        <LanguageSelect 
            config={global.config} 
            expandedNum={2}
            values = {values}
            onChange={onChange}
            collection='SortedLanguage'
        />); //完全渲染

        wrapper.props().onChange([1,2,3])
        wrapper.update();

        expect(values).toEqual([1,2,3])
    });
});

过程中遇到的问题,如何解决?

1、备案系统引用的公共模块(比如组件库,样式库)是通过webpack定义externals,将挂载到window全局变量中的变量,赋值给对应的模块名。jest中如何识别,测试用例代码中怎么引用这些公共模块?

原因:jest是node环境运行,全局变量是global,获取不到浏览器中的全局变量window

我的方法:

  • npm下载这些模块的包
  • 配置在jest的config中的moduleNameMapper
2、定义别名,webpack中是通过alias来定义目录的别名,jest如何与目前文件引用名保持一致定义?


3、api异步调用接口如何模拟?

原因: api返回的结构是promise
我的方法: 加载babel-polyfill包,使用es6中的promise,让api返回promise

4、react基础组件如何测试?
  • 基础组件主要包括:表单类基础组件、静态样式类组件、路由分配组件等
  • 表单类基础组件:模拟onChange方法,验证onChange触发后 values的值是否符合预期。(疑问🤔️:如果模拟页面点击来触发onChange/onSelect等函数?)
  • 静态样式类组件:生成组件快照,同时验证dom元素中必要的内容是否符合预期
  • 路由分配组件:暂时还没有思路,待研究

接下来的完善计划有哪些?

1、完善测试用例 提高覆盖率
2、将测试用例提交到git上,发布编译时,过滤掉测试用例
3、学习git CI/CD 将单元测试与此结合使用。

学习过程中参考的文章:

⭐️Jest api文档

⭐️Enzyme api 文档

使用Enzyme和Jest测试React组件

扔掉Create React App,打造你自己的React生成工具!

React测试框架之enzyme

⭐️Jest & enzyme 进行react单元测试

使用Jest进行React单元测试

⭐️详细的jest配置解释

posted @ 2021-01-04 17:02  happyYawen  阅读(396)  评论(0编辑  收藏  举报