jest自动化测试

概念#

  • 单元测试,只对某个功能点进行测试
  • 集成测试,对单元以及单元里面包含的其它东西统一的测试,包括其它的依赖

中文官网:jest
umitest:umi-test 单元测试使用记录

所有示例为测试通过,目的举一反三

测试文件#

jest运行时会遍历项目中*.test.js文件,所以测试文件取名可以参照这种格式。

测试文件名举例:

index.test.js
mark.test.js
link.test.js
comments.test.js

在jest环境中文件内容默认只支持common.js书写规范,如需es6写法需要npm或yarn安装@bable/core @bable/preset-env 新增配置文件babel.config.json和配置

{
    "presets": ["@babel/preset-env"]
}

基础函数#

test() 或 it()#

测试函数,每一项测试都需要一个test函数。it是简写

expect() 和 toBe()#

expect(预期)某项事物函数或者值和toBe(绝对匹配),和===作用相同

//用jest验证add函数中1+2=3
//原函数add
function add(a,b){
  return a+b;
}
//jest验证
test('验证add函数',()=>{
  expect(add(1,2)).toBe(3);
});

浮点运算请看下面toBeCloseTo()

toEqual()#

预期,正确值匹配

const a = {
    name:"iqszlong",
};
const b = {
    name:"iqszlong",
};

test('验证对象相同',()=>{
  expect(a).toEqual(b); //a与b是相同的对象
});

toBeNull()、toBeUndefined()、toBeDefined()、toBeTruthy()、toBeFalsy()#

Null值匹配、undefined匹配、已定义匹配,真匹配、假匹配

const a = null;
const b = undefined;
const c = '';
const d = true;
const e = false;

test('验证null,undefined、已定义、真和假',()=>{
  expect(a).toBeNull();                //a === null
  expect(b).toBeUndefined();    //b === undefined
  expect(c).toBeDefined();        //c !== undefined
  expect(d).toBeTruthy();            //d === true
  expect(e).toBeFalsy();            //e === false
});

toBeGreaterThan()、toBeGreaterThanOrEqual()、toBeLessThan()、toBeLessThanOrEqual()#

大于、大于等于、小于、小于等于

const a = 1;
const b = 2;
const c = 3;

test('验证大于、大于等于、小于、小于等于',()=>{
  expect(b).toBeGreaterThan(a);       //b > a
  expect(a).toBeGreaterThanOrEqual(a);//a >= a
  expect(a).toBeLessThan(c);          //a < c
  expect(b).toBeLessThanOrEqual(c);   //b <= c
});

toBeCloseTo()#

接近,近似值

function add(a,b){
  return a+b;
}

test('验证add函数',()=>{
  expect(add(1.2,2.1)).toBeCloseTo(3.3); //js浮点计算误差 (1.2+2.1 != 3.3)
});

toMatch()#

检查字符串是否含有toMatch函数的参数,可使用正则表达式。

const a = '鲁迅说:我没说过';

test('验证是否包含字符串 鲁迅',()=>{
  expect(a).toMatch(/鲁迅/); //a匹配到字符串‘鲁迅’
});

test('验证是否包含字符串 没说过',()=>{
  expect(a).toMatch('没说过'); //a匹配到字符串‘没说过’
});

toContain()#

检查一个数组或可迭代对象是否包含某个特定项

const a = ['胡不笑','鲁迅','大禹'];
const b = new Set(a);

test('验证是否包含 鲁迅',()=>{
  expect(a).toContain('鲁迅'); //a数组包含‘鲁迅’
});

test('验证是否包含 大禹',()=>{
  expect(b).toContain('大禹'); //b包含‘大禹’
});

toThrow()#

抛出错误,验证函数是否抛出错误以及错误提示是否匹配。参数可使用正则表达式。

function hasError() {
  throw new Error('错误');
}

test('验证抛出错误', () => {
  expect(()=>hasError()).toThrow(); //函数是否抛出错误
  expect(()=>hasError()).toThrow('错误'); //函数是否抛出相同的错误提示
  expect(()=>hasError()).toThrow(/错/); // 错误提示是否匹配到 ‘错’
});

异步测试#

//等待时间函数
function sleep(delay) {
  var start = (new Date()).getTime();
  while ((new Date()).getTime() - start < delay) {
    // 使用  continue 实现;
    continue;
  }
}
//主体函数
function main(fn) {
  sleep(1000);//等待1秒
  console.log('等待结束');
  fn('getData');
}
//开始测试
test('异步测试', (done) => {
  function get(res) {
    try {
      expect(res).toBe('getData');//验证等待1秒后得到的值
      done();
    } catch (error) {
      done(error);
    }
  }
  main(get);//调用main函数,传入get方法
});

异步请求#

function getPost() {
    return fetch('https://v1.hitokoto.cn');
}

test('请求测试', async () => {
  try {
    const response = await getPost();
    const data = await response.json();
       expect(data.id).not.toBeNull();
  } catch (error) {
    expect(error.toString().indexof('404') > -1).toBeTruthy();
  }

});

  
test('请求测试2', () => {
  return getPost().then((response) => response.json()).then((data) => {
    expect(data.id).not.toBeNull();
  }).catch((e) => {
    expect(e.toString().indexof('404') > -1).toBeTruthy();
  });
});

强制执行数次#

增加expect断言,可以强制执行expect。

expect.assertions(2); //强制执行2次expect
test('请求测试2', () => {
  return getPost().then((response) => response.json()).then((data) => {
    expect(data.id).not.toBeNull();
  }).catch((e) => {
    expect(e.toString().indexof('404') > -1).toBeTruthy();
  });
});

钩子#

beforeAll(() => {
  // 在所有测试用例开始前需要做的工作
})

let counter
beforeEach(() => {
  // 保证每次执行都创建新的 Counter 实例
  // 避免不同测试用例之间产生影响 导致测试结果出现偏差
  counter = new Counter()
})

afterEach(() => {
  // 每个测试用例执行之后
})

afterAll(() => {
  // 在所有测试用例结束之后
})

作用域(分组)#

describe可嵌套

describe('简单测试', () => {
  function getPost() {
    return fetch('https://v1.hitokoto.cn');
  }

  test('请求测试', async () => {
    try {
      const response = await getPost();
      const data = await response.json();
      expect(data.id).not.toBeNull();
    } catch (error) {
      expect(error.toString().indexof('404') > -1).toBeTruthy();
    }

  });

});

执行顺序:先执行外层的 beforeEach 再执行内层的 beforeEach

beforeAll(() => console.log('1 - beforeAll'))
afterAll(() => console.log('1 - afterAll'))
beforeEach(() => console.log('1 - beforeEach'))
afterEach(() => console.log('1 - afterEach'))
test('', () => console.log('1 - test'))
describe('Scoped / Nested block', () => {
  beforeAll(() => console.log('2 - beforeAll'))
  afterAll(() => console.log('2 - afterAll'))
  beforeEach(() => console.log('2 - beforeEach'))
  afterEach(() => console.log('2 - afterEach'))
  test('', () => console.log('2 - test'))
})

// 1 - beforeAll
// 1 - beforeEach
// 1 - test
// 1 - afterEach
    // 2 - beforeAll
// 1 - beforeEach
    // 2 - beforeEach
    // 2 - test
    // 2 - afterEach
// 1 - afterEach
    // 2 - afterAll
// 1 - afterAll

只运行当前测试#

const a = '鲁迅说:我没说过';

//只运行这个测试
test.only('验证是否包含字符串 鲁迅',()=>{
  expect(a).toMatch(/鲁迅/); //a匹配到字符串‘鲁迅’
});
//该测试不会运行
test('验证是否包含字符串 没说过',()=>{
  expect(a).toMatch('没说过'); //a匹配到字符串‘没说过’
});

Mock函数模拟#

describe('模拟函数基本', () => {
  test('测试jest.fn()返回固定值', () => {
    let mockFn = jest.fn().mockReturnValue('default');
    // 断言mockFn执行后返回值为default
    expect(mockFn()).toBe('default');
  });

  test('测试jest.fn()返回Promise', async () => {
    let mockFn = jest.fn().mockResolvedValue('default');
    let result = await mockFn();
    // 断言mockFn通过await关键字执行后返回值为default
    expect(result).toBe('default');
    // 断言mockFn调用后返回的是Promise对象
    expect(Object.prototype.toString.call(mockFn())).toBe("[object Promise]");
  })
});


describe('简单模拟函数测试', () => {
  //原函数add
  function add(a, b) {
    return a + b;
  }

  //模拟函数
  const mockadd = jest.fn((a, b) => {
    return add(a, b)
  });

  test('模拟测试', () => {
    let result = mockadd(1, 2);
    expect(mockadd).toBeCalled(); //模拟函数已被调用
    expect(mockadd).toBeCalledTimes(1); //模拟函数被调用过1次
    expect(mockadd).toHaveBeenCalledWith(1, 2); //模拟函数的参数为1,2
    expect(result).toBe(3); //验证模拟函数结果
  });

});

使用外部文件测试#

data.js

const mockFn = {
    getData: async function (fn) {
        const res = await this.getPost();
        const data = await res.json();
        return fn(data);
    },
    getPost: function () {
        return fetch('https://v1.hitokoto.cn');
    },
}
export default mockFn;

测试文件

describe('外部函数测试', () => {
  test('代理测试', async () => {
    expect.assertions(1);
    let mockReq = jest.fn();
    await mockFn.getData(mockReq);
    expect(mockReq).toBeCalled();
  });
});

模拟模块#

users.js

import axios from 'axios';

class Users {
  static all() {
    return axios.get('/users.json').then(resp => resp.data);
  }
}

export default Users;

测试文件

import axios from 'axios';
import Users from './users';

jest.mock('axios');

test('should fetch users', () => {
  const users = [{name: 'Bob'}];
  const resp = {data: users};
  axios.get.mockResolvedValue(resp);

  // or you could use the following depending on your use case:
  // axios.get.mockImplementation(() => Promise.resolve(resp))

  return Users.all().then(data => expect(data).toEqual(users));
});

React组件测试#

首先定义一个组件,作用是输出一个按比例显示的盒子RatioBox。传入属性宽(width:16)和高(height:9)计算出比例值放在style中作为样式输出。注意:宽高不带单位只有数值

等比输出样式参照Bootstrap 3 的一个组件写法:响应式媒体内容

import React from 'react';
import Style from './style.css';

class RatioBox extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      ratio: (9 / 16) * 100,
    };
  }
  static getDerivedStateFromProps(props, state) {
    const data = state;
    //渲染之前修改state值
    if (props.width !== undefined && props.height !== undefined) {
      data.ratio = (props.height / props.width) * 100;
      return data;
    }
    return null;
  }

  shouldComponentUpdate(newProps, newState) {
    return this.props !== newProps;
  }

  render() {
    return (
      <>
        <div
          className={`${Style.layout} ${this.props.className || ''}`}
          style={{ paddingTop: `${this.state.ratio}%` }}
        >
          <div className={Style.inner}>{this.props.children}</div>
        </div>
      </>
    );
  }
}

export default RatioBox;

组件有了,现在来编写测试。

使用原始React测试#

React测试请参考官网:测试技巧-数据获取

由于测试环境需要act()来渲染,dom结构和属性都需要使用javaScript原生支持写法才能获取。

获取dom元素属性:MDN-WebAPI-attribut

选择dom元素:MDN-WebAPI-querySelector

import RatioBox from '..';
import renderer from 'react-test-renderer';
import { render, unmountComponentAtNode } from "react-dom";
import { act } from "react-dom/test-utils";

let container = null;
beforeEach(() => {
    // 创建一个 DOM 元素作为渲染目标
    container = document.createElement("div");
    document.body.appendChild(container);
});

afterEach(() => {
    // 退出时进行清理
    unmountComponentAtNode(container);
    container.remove();
    container = null;
});

describe('React 渲染测试', () => {
    test('渲染默认值', () => {
        act(() => {
            render(<RatioBox />, container);
        });
        const wrapper = container.querySelectorAll('div')[0]; //获取最外层div
        const cssName = wrapper.attributes[0];  //获取第一个属性 className
        const styleName = wrapper.attributes[1];//获取第二个属性style
        
        expect(cssName.value).toMatch("layout");//验证 className 的值
        expect(styleName.value).toMatch("56.25%");//验证计算后的 style 的值
    });
});s

使用enzym测试#

组件测试需要包 enzym

yarn add enzyme enzyme-to-json enzyme-adapter-react-16 -D

jest.setup.js中增加enzym配置,使jest使用enzym

const Enzyme = require('enzyme');
const { shallow, render, mount } = Enzyme;
const Adapter = require('enzyme-adapter-react-16');

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

global.shallow = shallow;
global.render = render;
global.mount = mount;

简单组件测试

import RatioBox from '..';
import renderer from 'react-test-renderer';
import { shallow } from 'enzyme';

describe('属性测试', () => {
    test('className', () => {
        const wrapper = shallow(<RatioBox className="test_css" width="10" height="5" />);
        console.log(wrapper.debug());//调试html输出
        const outerLayer = wrapper.find('.layout');
        console.log(outerLayer.debug());//调试html输出
        expect(outerLayer.prop('className')).toMatch('test_css');
        expect(outerLayer.prop('style')).toEqual({paddingTop:"50%"});
    });
});
  • shallow()浅渲染器
  • enzym选择器,类似jQuery选择器,
  • render静态渲染器,生成html树和分析代码
  • mount()挂载到dom中,多个内容会互相影响,需要.unmount()进行清理。

生成测试覆盖率报告#

npx jest --coverage

umi#

umi test --coverage

测试报告会生成/coverage目录,/coverage/lcov-report/index.html就是报告文件。

package.json#

在文件package.json中的scripts下增加运行命令

"scripts": {
    "coverage": "umi test --coverage", 
    // 或者 
    "coverage": "jest --coverage"
},

自动监听测试#

package.json中的scripts`下增加运行命令

"scripts": {
    "autotest": "jest --watchAll"
},

问题集锦#

fetch、Request测试报undefined#

参考github:fetch 用 jest 运行测试时未定义

在jest.config.js文件中增加代码:

module.exports = {
    setupFilesAfterEnv: ['./jest.setup.js'],
};

再jest.setup.js文件中增加代码:

const {Response, Request, Headers, fetch} = require('whatwg-fetch');
global.Response = Response;
global.Request = Request;
global.Headers = Headers;
global.fetch = fetch;

注意:文件都在项目根目录中

jest.config.js文件可以在项目新建(umi下存在jest模块,新建文件即可被读取),也可以使用命令行自动创建。(自动创建会包含默认配置,影响umi已存在配置)

npx jest --init
-> jsdom //web环境用jsdom
-> coverage report  //覆盖率报告,umi已有输入n,其他环境请自行考虑
-> Auto mock clear //自动清除mock临时数据,选n
-> 生成文件jest.config.js