【学习】前端测试 jest (一)

251 阅读8分钟

先对这个进行简要的说明:

我所在的行业是传统行业,公司并没有很多项目,一般有的项目都是增加功能和维护,由于每一次增加功能都可能导致修改了以前的逻辑或者界面什么,毕竟我刚来的时候项目已经差不多完成了,那么有一个前端测试就很有必要,即便将来我辞职了也可以放心的交给接替我的人。反正我觉得只要是需要反复迭代的产品有一个单元测试或集成测试都是非常有必要的,特别是你不全明白代码逻辑。

这里有一个掘金上的大佬说的文章,我觉得写得很好,分享给大家,同时感谢作者:React Hook测试指南

安装的部分也就不说了,我把官网的地址发出来方便查看: Getting Started 。主要是针对 react-native 的,只不过 react 也大同小异。 react-native 的如果版本高的话,默认都是有的,前提是你使用 react-native init 项目名 装的项目。测试代码不需要把项目跑起来。

1. 纯函数的逻辑测试

1.1 带有返回值的函数测试

1.1.1 非引用类型

number 首先测试加法,函数的定义如下:

const add = (...digits) => {
  let sum = 0;
  for (let i = 0; i < digits.length; i++) {
    sum += digits[i];
  }
  return sum;
};

这个函数我们知道,如果要想测试彻底,能想到的可能有下面的几种:

  1. 传入的为空;
  2. 传入的为一个元素(小数,整数,负数)
  3. 传入的是多个(小数,整数, 负数)
// 1. 传入为空
it('测试 add 函数的准确性()=>0', () => {
  expect(add()).toBe(0);
});
// 2. 传入的为一个元素(小数,整数,负数)
it('测试 add 函数的准确性(1)=>1', () => {
  expect(add(1)).toBe(1);
});

it('测试 add 函数的准确性(-5)=>-5', () => {
  expect(add(-5)).toBe(-5);
});

it('测试 add 函数的准确性(-0.0003)=>-0.0003', () => {
  expect(add(-0.0003)).toBe(-0.0003);
});
// 3. 传入的是多个(小数,整数, 负数)
it('测试 add 函数的准确性(1,1)=>2', () => {
  expect(add(1, 1)).toBe(2);
});

it('测试 add 函数的准确性(-1,-1,2,4,6)=>10', () => {
  expect(add(-1, -1, 2, 4, 6)).toBe(10);
});

it('测试 add 函数的准确性(0.1, 0.1, 0.2)=>0.4', () => {
  expect(add(0.1, 0.1, 0.2)).toBe(0.4);
});
// 这个如果我们仍然使用 toBe 的话会发现这个并不能通过,像这种需要使用 toBeCloseTo 
it('测试 add 函数的准确性(-0.1, 0.2, 0.3, -0.1)=>0.3', () => {
  expect(add(-0.1, 0.2, 0.3, -0.1)).toBeCloseTo(0.3);
});

下面是关于浮点数的说明:

对于比较浮点数相等,使用 toBeCloseTo 而不是 toEqual,因为你不希望测试取决于一个小小的舍入误差。

test('两个浮点数字相加', () => {
  const value = 0.1 + 0.2;
  //expect(value).toBe(0.3);           这句会报错,因为浮点数有舍入误差
  expect(value).toBeCloseTo(0.3); // 这句可以运行
});

这些都测试通过以后,这个函数也差不多了,但是实际情况肯定不值这些,至少还有一个情况没有想到,那就是当通过加法处理以后溢出的情况,在这里我就暂时不考虑的了,具体这一块需要按业务需要进行,比如溢出以后返回什么,可以是抛出错误,也可以返回具体的值。 boolean 先看函数:

// 判断传入的对象获取数组是否为空
const isEmpty = (objOrArr) => {
  return Object.keys(objOrArr).length === 0;
};

这个函数我们知道,如果要想测试彻底,能想到的可能有下面的几种:

  1. 传入的为空;
  2. 传入的对象或者数组不为空的情况
// 1. 传入的为空;
it('测试 isEmpty 函数的准确性([])=>true', () => {
  expect(isEmpty([])).toBe(true);
});

it('测试 isEmpty 函数的准确性({})=>true', () => {
  expect(isEmpty({})).toBe(true);
});
// 2. 传入的对象或者数组不为空的情况
it('测试 isEmpty 函数的准确性({name: "吴敬悦"})=>false', () => {
  expect(isEmpty({name: '吴敬悦'})).toBe(false);
});

it('测试 isEmpty 函数的准确性([1, 2])=>false', () => {
  expect(isEmpty([1, 2])).toBe(false);
});

还可以使用下面的方式:

it('测试 isEmpty 函数的准确性({})=>true', () => {
  expect(isEmpty({})).toBeTruthy();
});

it('测试 isEmpty 函数的准确性({name: "吴敬悦"})=>false', () => {
  expect(isEmpty({name: '吴敬悦'})).toBeFalsy();
});

只不过 toBeTruthy 和 toBeFalsy 不仅仅是这样的,而是通过下面的值来判定的,false, 0, '', null, undefined, 和 NaN这几个都是使用 toBeFalsy ,其余的都是 toBeTruthy 。

其他的基本数据类型就一一练习了。

1.1.2 引用类型

先看函数定义:

const sort = (arr) => {
  return arr.sort((a1, a2) => a1 - a2);
};

这个函数我们知道,如果要想测试彻底,能想到的可能有下面的几种:

  1. 传入的为空;
  2. 传入的为一个元素
  3. 传入的元素多个
// 1. 传入的为空;
it('测试 sort 函数的准确性([])=>[]', () => {
  expect(sort([])).toEqual([]);
});
// 2. 传入的为一个元素
it('测试 sort 函数的准确性([9])=>[9]', () => {
  expect(sort([9])).toEqual([9]);
});
// 3. 传入的元素多个
it('测试 sort 函数的准确性([1, 9, 7])=>[1, 7, 9]', () => {
  expect(sort([1, 9, 7])).toEqual([1, 7, 9]);
});

it('测试 sort 函数的准确性([-1, 0.99, -1.2])=>[-1.2, -1, 0.99]', () => {
  expect(sort([-1, 0.99, -1.2])).toEqual([-1.2, -1, 0.99]);
});

这里不能使用 toBe 的方式,需要使用 toEqual 的方式,这种方式会递归比较。而前者只会比较值,对于引用来说值只是一块地址,地址相同则相等。

下面再来一个函数:

const getRandomIntArr = (len, start, end) => {
  let arr = [];
  for (let i = 0; i < len; i++) {
    arr.push(Math.round(Math.random() * (end - start) + start));
  }
  return arr;
};

为了验证这个函数的准确性,我觉得应该从以下两点入手:

  1. 首先得到的数组的长度是否正确;
  2. 得到的数组中的每一个元素是否符合规定。
it('测试 getRandomIntArr 函数的准确性(10, 2, 8)=>[...]', () => {
  let len = 10;
  let arr = getRandomIntArr(len, 2, 8);
  expect(arr.length).toBe(len);
  for (let i = 0; i < len; i++) {
    expect(arr[i]).toBeGreaterThanOrEqual(2);
    expect(arr[i]).toBeLessThanOrEqual(8);
  }
});

其中 :

  • toBeGreaterThanOrEqual 是 >= 的意思,对应的 > 为: toBeGreaterThan ;
  • toBeLessThanOrEqual 是 <= 的意思,对应的 < 为: toBeLessThan 。

其他的还有很多方法,需要的时候再学习,先把地址放到这里: Expect Methods

1.2 不带有返回值的函数的测试

没有返回的函数一般都是对引用数据的处理,传入的数据会发生改变;

先看看函数:

const push = (arr, ...data) => {
  arr.push(...data);
};

然后再看看测试:

it('测试无返回值的函数', () => {
  let arr = [];
  push(arr, 10, 11, 12);
  expect(arr).toEqual([10, 11, 12]);
});

2. 异步函数的逻辑测试

这里我使用定时器模拟,在以后的实战中我会搭建简单的 nodejs 服务器模拟真实的网络请求。

首先看看异步的方法:

export const getList = ({timeout = 3000}) =>
  new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve([1, 2, 3, 4, 5]);
    }, 1000);
    setTimeout(() => {
      reject(new Error('获取信息失败'));
    }, timeout);
  });

然后写出测试,根据传入的值的不同得到不同的结果

it('测试异步方法', async () => {
  try {
    let res = await getList({timeout: 2000});
    expect(res).toEqual([1, 2, 3, 4, 5]);
  } catch (err) {
    expect(err.message).toMatch('获取信息失败');
  }
});

有时候我们对后台的返回值大部分没有准确的把握,但是对于前端来说,我们需要的字段一定得有或者返回的某一个字段一定是前端的某一个值。

重新定义了一个函数:

const getUserInfo = ({timeout = 3000}) =>
  new Promise((resolve, reject) => {
    let timer1 = setTimeout(() => {
      timer1 && clearTimeout(timer1);
      resolve({
        _id: '5f23789273',
        username: '吴敬悦',
        sex: '男',
        position: '前端工程师',
      });
    }, 1000);
    let timer2 = setTimeout(() => {
      timer2 && clearTimeout(timer2);
      reject(new Error('获取信息失败'));
    }, timeout);
  });

接下来我验证返回的所有的字段中是否包含函数中的那几个字段:

it('测试异步方法中的返回的数据是否包含前端需要的字段', async () => {
  try {
    let res = await getUserInfo({});
    expect(res).toEqual(
      expect.objectContaining({
        _id: expect.any(String),
        username: expect.any(String),
        sex: expect.any(String),
        position: expect.any(String),
      }),
    );
  } catch (err) {
    expect(err.message).toMatch('获取信息失败');
  }
});

这样就达到我想要的目的了,如果你想要的数据中有一个字段是确定的值,那么只需要将对应的类型换成具体的值即可。像下面:

it('测试异步方法中的返回的数据是否包含前端需要的字段和某一个字段必须是具体的值', async () => {
  try {
    let res = await getUserInfo({});
    expect(res).toEqual(
      expect.objectContaining({
        _id: '5f23789273',
        username: expect.any(String),
        sex: expect.any(String),
        position: expect.any(String),
      }),
    );
  } catch (err) {
    expect(err.message).toMatch('获取信息失败');
  }
});

还有一种情况,前端不知道什么情况,给后台的传入的字段不对,包括类型或字段名称,前一种情况可能是后端返回的数据不正确导致的,那么这种情况就是作为前端的我们应该做的了,毕竟我们得保证自己传入的数据是正确的。重新实现一个函数:

const create = ({timeout = 3000, name, type}) =>
  new Promise((resolve, reject) => {
    let timer1 = setTimeout(() => {
      timer1 && clearTimeout(timer1);
      resolve({
        _id: '5f23789273',
        username: '吴敬悦',
        sex: '男',
        position: '前端工程师',
        name,
        type,
      });
    }, 1000);
    let timer2 = setTimeout(() => {
      timer2 && clearTimeout(timer2);
      reject(new Error('获取信息失败'));
    }, timeout);
  });

然后看看测试:

const callback = (fn) => {
  fn({name: 'sha', type: 1});
};
it('测试方法中的前端传入的字段的准确性', async () => {
  const fn = jest.fn(create);
  callback(fn);
  expect(fn).toBeCalledWith(
    expect.objectContaining({
      name: expect.any(String),
      type: expect.any(Number),
    }),
  );
});

感觉这种没必要,因为我测试的时候自己填写参数,这种情况都错了或者对了,对实际情况就什么影响,最好是程序在运行过程中所传递的参数才有意义,这一块到时候实战的时候会遇到的,如果遇到了就补充。

3. redux的测试和useReducer

由于这两个是差不多的,所以我就只说 redux 的了。 首先创建相关文件

.
├── TestFunction
│   ├── index.js
│   ├── index.test.js
│   └── styles.js
└── app
    ├── actions
    │   ├── counts.js
    │   └── counts.test.js
    ├── reducers
    │   ├── counts.js
    │   ├── counts.test.js
    │   └── index.js
    └── types
        └── counts.js

其中我写的测试在 actions>counts.test.js 和 reducers>counts.test.js 中。 先看 reducers 中的 counts.js 文件:

import {ADD, DIVISION, MUL, SUB} from '../types/counts';
const initState = 0;
const counts = (state = initState, actions) => {
  switch (actions.type) {
    case ADD:
      return state + actions.count;
    case SUB:
      return state - actions.count;
    case MUL:
      return state * actions.count;
    case DIVISION:
      return state / actions.count;
    default:
      return state;
  }
};
export default counts;

这个的测试写起来很简单,主要是考虑好各种情况,以便都测试到。

const {ADD, SUB, MUL, DIVISION} = require('../types/counts');
const {default: counts} = require('./counts');

it('测试 reducers 中的默认值', () => {
  expect(counts(undefined, {})).toBe(0);
});

it('测试 reducers 中的 add', () => {
  expect(counts(0, {type: ADD, count: 1})).toBe(1);
});

it('测试 reducers 中的 sub 整数的情况', () => {
  expect(counts(2, {type: SUB, count: 1})).toBe(1);
});

it('测试 reducers 中的 sub 小数的情况', () => {
  expect(counts(0.3, {type: SUB, count: 0.1})).toBeCloseTo(0.2);
});

it('测试 reducers 中的 mul', () => {
  expect(counts(0.3, {type: MUL, count: 0.1})).toBeCloseTo(0.03);
});

it('测试 reducers 中的 division', () => {
  expect(counts(0.3, {type: DIVISION, count: 0.2})).toBeCloseTo(1.5);
});

在没有对数字有要求的情况下,一般都使用 toBeCloseTo 不然到时候还要改,哈哈。 toBeCloseTo 这个函数还有第二个参数,这个参数是保留几位小数。 下面看 actions 中的 counts.js :

import {ADD, DIVISION, MUL, SUB} from '../types/counts';

export const add = (count) => ({type: ADD, count});
export const sub = (count) => ({type: SUB, count});
export const mul = (count) => ({type: MUL, count});
export const division = (count) => ({type: DIVISION, count});

测试文件也非常简单,代码如下:

import {ADD, DIVISION, MUL, SUB} from '../types/counts';
import {add, division, mul, sub} from './counts';

it('测试加法的情况', () => {
  expect(add(1)).toEqual({
    type: ADD,
    count: 1,
  });
});

it('测试减法的情况', () => {
  expect(sub(-1)).toEqual({
    type: SUB,
    count: -1,
  });
});

it('测试乘法的情况', () => {
  expect(mul(10)).toEqual({
    type: MUL,
    count: 10,
  });
});

it('测试除法的情况', () => {
  expect(division(20)).toEqual({
    type: DIVISION,
    count: 20,
  });
});

下面是: 前端测试 jest 的学习(二)(每日计划)