前端测试 JEST

前言: 问了好些前端大神,回复都说,做好前端测试是个不简单的事情,特别是对于前端UI部分的测试。antd在UI上,可以说做得很好,但是看了他们的相关测试,也没有很完善。.

目前来说,前端测试在service和model层的应用,结合相关资料和自己遇到的坑,给大家分享一下。

一、配置

(1) jest的版本选择("jest": "^20.0.0"):

项目中,用到的 babel等编译工具不是最新版本(6.0.0+),在当前最新版本24.0.0下,会报编译工具不合适的错误,把版本降至20.0.0,可以使用。具体版本,可以根据项目中版本进行选择。

(2) .babelrc 的配置

项目中的预编译,主要是由babelrc 文件中配置,presets和plugins两项,这两项跟项目中,webpack中的配置保持一致

presets: ['es2015', 'stage-3'],
plugins: ['transform-class-properties',
          'syntax-dynamic-import',
          'transform-runtime'
         ],
复制代码
(3) jest.config 的配置

如果项目中在node_modules中依赖了未编译的插件,需要进行下面的设置。否则,在.test用例中,无法引入依赖,从而会报错。

  transform: {    // 将.js后缀的文件使用babel-jest处理
    "^.+\\.js$": "babel-jest",
  },
  transformIgnorePatterns: // 未编译的依赖,需要在这边配置 ["<rootDir>/node_modules/(?!(snk-sse|javascript-state-machine))"],
复制代码
(4)package 的配置
"jest": "jest --coverage"
复制代码

运行npm run jest,就会运行带有.test.js的所有文件

二、使用

(1)调用模拟接口

在node里面,没有实际接口调用,如果测试涉及到接口调用的返回数据,就需要进行模拟数据。下面,介绍其中一种比较简单的方法:

第一步,在 VideoService.js的同级目录,新建 mocks 文件夹,然后在文件夹里面,新建 VideoService.js(mock文件夹的文件名必须与的实际文件名一致)

// __mocks__/VideoService.js文件,
export default {
  httpQuery() {
    return new Promise((resolve)=>{
      resolve(123123);
    })
  }
}
复制代码

第二步,在测试test.js文件中,需要加上jest.mock(),RtVideoService.httpChange调用了VideoService.js的接口,这里会查找__mocks__下面的文件,并使用VideoService模拟的数据

jest.mock('../services/VideoService.js'); // 必须存在,要不然会报错
test('RtVideoService', () => {
  const obj = { 
    newer: '[email protected]', 
    callback: { error: jest.fn() }
  };
  await RtVideoService.httpChange(null, obj); // 使用了__mocks__下的文件
  expect(obj.callback.next.mock.calls.length).toBe(1);
})

复制代码
(2)运行单条测试用例

如果测试失败,第一件要检查的事就是,当仅运行这条测试时,它是否仍然失败。 在 Jest 中很容易地只运行一个测试 — — 只需暂时将 test 命令更改为 test.only:

test.only('this will be the only test that runs', () => {
  expect(true).toBe(false);
});

test('this test will not run', () => { // 此条不会运行
  expect('A').toBe('A');
});
复制代码
(3)常用的断言

普通匹配器

  • .toBe - toBe 使用 Object.is 来测试是否完全相等
  • .not - 用来测试相反的用例
  • .toEqual - 如果你想检查某个对象的值,请改用 toEqual。
test('two plus two is four', () => { 
    expect(2 + 2).toBe(4); 
});
test('object assignment', () => { 
    const data = {one: 1}; 
    data['two'] = 2; 
    expect(data).toEqual({one: 1, two: 2}); 
});
复制代码

布尔值匹配器

  • toBeNull 只匹配 null
  • toBeUndefined 只匹配 undefined
  • toBeDefined 与 toBeUndefined 相反
  • toBeTruthy 匹配任何 if 语句为真
  • toBeFalsy 匹配任何 if 语句为假
test('null', () => {
  const n = null;
  expect(n).toBeNull();
  expect(n).toBeDefined();
  expect(n).not.toBeUndefined();
  expect(n).not.toBeTruthy();
  expect(n).toBeFalsy();
});

test('zero', () => {
  const z = 0;
  expect(z).not.toBeNull();
  expect(z).toBeDefined();
  expect(z).not.toBeUndefined();
  expect(z).not.toBeTruthy();
  expect(z).toBeFalsy();
});
复制代码

字符串匹配器

  • toMatch - 正则表达式的字符
  • toHaveLength(number) - 判断一个有长度的对象的长度

数字匹配器

  • .toBeGreaterThan() - 大于
  • .toBeGreaterThanOrEqual() 大于等于
  • .toBeLessThan() - 小于
  • .toBeLessThanOrEqual() - 小于等于
  • .toBeCloseTo() - 浮点数比较

数组匹配器

  • .toContain(item) - 判断数组是否包含特定子项
  • .toContainEqual(item) - 判断数组中是否包含一个特定对象

自定义断言

expect.extend({
  toBeDivisibleBy(received, argument) {
    const pass = received % argument == 0;
    if (pass) {
      return {
        message: () =>
          `expected ${received} not to be divisible by ${argument}`,
        pass: true,
      };
    } else {
      return {
        message: () => `expected ${received} to be divisible by ${argument}`,
        pass: false,
      };
    }
  },
});

test('even and odd numbers', () => {
  expect(100).toBeDivisibleBy(2);
  expect(101).not.toBeDivisibleBy(2);
});
复制代码
(4)上面有说到模拟接口,其他模拟方法

1、jest.fn()

// 举例
test('测试jest.fn()调用', () => {
  let mockFn = jest.fn();
  let result = mockFn(1, 2, 3);

  // 断言mockFn的执行后返回undefined
  expect(result).toBeUndefined();
  // 断言mockFn被调用
  expect(mockFn).toBeCalled();
  // 断言mockFn被调用了一次
  expect(mockFn).toBeCalledTimes(1);
  // 断言mockFn传入的参数为1, 2, 3
  expect(mockFn).toHaveBeenCalledWith(1, 2, 3);
})

// 实际运用
test('answer', () => {
  const fn = { error: jest.fn() };
  ExeService.answer(null, fn);
  expect(fn.error.mock.calls.length).toBe(1); // 测试成功
});
复制代码

jest.fn()所创建的Mock函数还可以设置返回值,定义内部实现或返回Promise对象。

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

test('测试jest.fn()内部实现', () => {
  let mockFn = jest.fn((num1, num2) => {
    return num1 * num2;
  })
  // 断言mockFn执行后返回100
  expect(mockFn(10, 10)).toBe(100);
})

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]");
})
复制代码

2、jest.mock(),改变函数的内部实现,上面已经有实际案例,不估重复讲述

3、jest.spyOn()

jest.spyOn()方法同样创建一个mock函数,但是该mock函数不仅能够捕获函数的调用情况,还可以正常的执行被spy的函数。实际上,jest.spyOn()是jest.fn()的语法糖,它创建了一个和被spy的函数具有相同内部代码的mock函数

jest.mock('../services/VideoService.js'); // 必须存在,要不然会报错
test('RtVideoService', () => {
  const obj = { 
    newer: '[email protected]', 
    callback: { error: jest.fn() }
  };
  await RtVideoService.httpChange(null, obj); // 使用了__mocks__下的文件
  const spyFn = jest.spyOn(VideoService, 'httpChange');
  expect(spyFn).toHaveBeenCalled();
  expect(obj.callback.next.mock.calls.length).toBe(1);
})
复制代码

三、覆盖率检查

在执行完写的测试用例后,你可能会想,有什么指标来说明我写的测试用例,覆盖项目全不全?

"test": "jest --coverage"
复制代码

1、首先,在package的命令上,带有coverage,这时,执行成功全部用例,会输出以下结果:

%stmts是语句覆盖率(statement coverage):是不是每个语句都执行了?

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

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

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

2、coverage 需要忽略的文件或文件夹

  coveragePathIgnorePatterns: [
    "\\\\node_modules\\\\",
    "<rootDir>/src/utils/",
    "<rootDir>/src/observers/",
    "<rootDir>/lib/",
  ],
复制代码

3、执行完测试用例后,会在项目要目录,生成coverage文件夹,里面输出覆盖率报告

四、从中得到的体会

  • 1、为了进行jest、 Mock,如果代码不适合测试,会对代码进行重构,这在一定程度使自己的代码结构更加趋于合理;
  • 2、单元测试可以给出每项测试的响应时间,合理划分的单元测试有助于定位代码的性能问题;
  • 3、单元测试还是一份很好的业务文档,每项测试的描述都可以体现业务逻辑

JEST 官网

猜你喜欢

转载自blog.csdn.net/weixin_34211761/article/details/91378990