前端测试框架Jest入门总结

背景及原理

为了降低上线的bug,使用TypeScript,Flow, Eslint ,StyleLint这些工具可以实现。前端自动化测试工具普及情况不是很好。测试分为单元测试,集成测试和端到端测试。单元测试主要是对一个独立的功能单元进行的测试,通过一个小例子来了解前端自动化测试的背景。

例如我们我们现在编写一个math.js

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

我们可以在其根目录再新建一个math.test.js进行对math的方法进行测试

function expect(result) {
  return {
    toBe: function(actual) {
      if (result !== actual) {
        throw new Error('预期值和实际值不匹配')
      }
    }
  }
}

function test(desc, fn) {
  try {
    fn();
    console.log(`${desc} 通过测试`)
  } catch (e) {
    console.log(`${desc} 没有通过测试 ${e}`)
  }
}

test('测试加法 3 + 7', () => {
  expect(add(7, 3)).toBe(10);
})

test('测试减法 3 - 3', () => {
  expect(minus(3, 3)).toBe(0);
})

优势

image-20210310134818196

安装

npm install jest

改造以上例子

math.js

function add(a, b) {
  return a + b;
}
function minus(a, b) {
  return a - b;
}
function multi(a, b) {
  return a * b;
}
module.exports = {
    add,
    minus,
    multi
}

math.test.js

const math = require('./math.js');
const { add, minus, multi } = math;

test('测试加法 3 + 7', () => {
  expect(add(7, 3)).toBe(10);
})

test('测试减法 3 - 3', () => {
  expect(minus(3, 3)).toBe(0);
})

将package.json中的

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

运行 npm run test就可以查看出模块是否测试通过

配置

使用一下命令,可以进行暴露jest默认的配置

npx jest --init

运行完后根目录会生成jest.config.js

  • 如何查看测试覆盖率,可以执行

    npx jest --coverage
    

    此时会根目录生成一个coverage目录

使用 Babel

要使用 Babel,请通过 npm的依赖项:

 npm i @babel/[email protected] @babel/[email protected] -D

在项目的根目录下创建 .babelrc ,通过配置 Babel 使其能够兼容当前的 Node 版本。

{
  "presets": [
    ["@babel/preset-env", {
      "targets": {
        "node": "current"
      }
    }]
  ]
}

匹配器

普通匹配器

toBe() 用于Object.is测试完全相等

test('two plus two is four', () => {
  expect(2 + 2).toBe(4);
});

toEqual 递归检查对象或数组的每个字段

test('object assignment', () => {
  const data = {one: 1};
  expect(data).toEqual({one: 1});
});

真假有关的匹配器

  • toBeNull 仅匹配 null
  • toBeUndefined 仅匹配 undefined
  • toBeDefined 与...相反 toBeUndefined
  • toBeTruthy匹配if语句视为真的任何内容
  • toBeFalsy匹配if语句视为假的任何内容
  • not 匹配器 取相反的值 !
test('null', () => {
  const n = null;
  expect(n).toBeNull();
  expect(n).toBeDefined();
  expect(n).not.toBeUndefined();
  expect(n).not.toBeTruthy();
  expect(n).toBeFalsy();
});

Number 匹配器

  • toBeGreaterThan 是否比匹配的值大
  • toBeLessThan 是否比匹配的值小
  • toBeGreaterThanOrEqual 是否大于或等于匹配值
  • toBeLessThanOrEqual 是否小于或等于匹配值
  • toBeCloseTo 判断浮点数相加时,js引起的浮点数溢出时,需使用
test('two plus two', () => {
  const value = 2 + 2;
  expect(value).toBeGreaterThan(3);
  expect(value).toBeGreaterThanOrEqual(3.5);
  expect(value).toBeLessThan(5);
  expect(value).toBeLessThanOrEqual(4.5);

  // toBe and toEqual are equivalent for numbers
  expect(value).toBe(4);
  expect(value).toEqual(4);
});

对于浮点相等,请使用toBeCloseTo代替toEqual,因为您不希望测试依赖于微小的舍入误差。

test('adding floating point numbers', () => {
  const value = 0.1 + 0.2;
  //expect(value).toBe(0.3);           This won't work because of rounding error
  expect(value).toBeCloseTo(0.3); // This works.
});

String匹配器

您可以使用toMatch以下命令针对正则表达式检查字符串:

test('toMatch', () => {
  const str = 'hello world';
  expect(str).toMatch(/world/);
})

Array和Set匹配器

可以使用以下命令检查数组或可迭代项是否包含特定项目toContain

test('toContain', () => {
  const arr = ['hello', 'world'];
  expect(arr).toContain('hello');
  expect(new Set(arr)).toContain('hello');
})

异常匹配器

如果要测试特定函数在调用时是否引发错误,请使用toThrow

const throwNewErrorFunc = function() {
  throw new Error('this is a new error.')
}
test('toThrow', () => {
  expect(throwNewErrorFunc).toThrow();
  expect(throwNewErrorFunc).toThrow(Error);

  // 如果你想判断错误的提示信息是否匹配
  expect(throwNewErrorFunc).toThrow('this is a new error.');
  expect(throwNewErrorFunc).toThrow(/error/);
})

命令行工具使用

  • 按a键运行所有测试。类似于 jest --watchAll
  • 按f键仅运行失败的测试。
  • 按o键只运行与更改文件相关的测试。类似于 jest --watch 【要在git环境下运行】
  • 按p键可按文件名regex模式进行筛选。
  • 按t键可通过测试名称regex模式进行筛选。
  • 按q键退出观看模式。
  • 按Enter键触发测试运行。

异步代码的测试方法

回调

当程序进行异步操作时,测试代码会不等待回调函数执行完毕,立马执行返回成功,如:

export const fetchData = (fn) => {
  axios.get('http://www.dell-lee.com/react/api/demo.json').then((res) => {
    fn(res.data)
  })
}

测试用例方法:

import { fetchData } from './fetchData';
// 回调类型异步函数的测试
test('fetchData 结果返回为 { success: true }', (done) => {
  fetchData((data) => {
    expect(data).toEqual({
      success: true
    })
    done()
  })
})

这里test的第二个参数需要加上done参数,等待done执行完毕,这样可以保证测试用例执行完毕

Promise

export const fetchData2 = (fn) => {
  return axios.get('http://www.dell-lee.com/react/api/demo1.json')
}

测试用例:

请确保返回断言,可以使用then方法,如果省略此return语句,则测试将在fetchData解析解决方案返回的承诺之前完成,然后then()有机会执行回调

test('fetchData2 结果返回为 { success: true }', () => {
  return fetchData2().then((res) => {
    expect(res.data).toEqual({
      success: true
    })
  })
})

返回是被拒绝的,使用catch方法。确保添加expect.assertions以验证是否调用了一定数量的expect。否则,兑现promise就不会使测试失败。

test('fetchData2 结果返回为 404', () => {
  expect.assertions(1); // expect 至少执行一次,才能通过测试 【使用catch时需使用】
  return fetchData2().catch(e => {
    expect(e.toString().indexOf('404') > -1).toBe(true)
  })
})

.resolves / .rejects

您也可以在.resolvesExpect语句中使用匹配器,Jest将等待该承诺解决。如果承诺被拒绝,则测试将自动失败。

test('fetchData2 resolves', () => {
  return expect(fetchData2()).resolves.toMatchObject({ // toMatchObject匹配器是包含有以下内容就为测试通过
    data: {
      success: true
    }
  })
})

请确保返回断言-如果忽略此return语句,则测试将在fetchData解析返回的promise之前完成,然后then()有机会执行回调

返回是被拒绝的-请使用.rejects匹配器。它与.resolves匹配器类似。如果promise得以兑现,则测试将自动失败。

test('fetchData2 rejects', () => {
  return expect(fetchData2()).rejects.toThrow()
})

Async/Await

要编写异步测试,请async在传递给的函数前面使用关键字test。例如,fetchData可以用以下方法测试相同的场景:

test('fetchData2 async await', async () => {
  const response = await fetchData2();
  expect(response.data).toEqual({
    success: true
  })
})

test('fetchData2 async await', async () => {
  expect.assertions(1);
  try {
    await fetchData2();
  } catch (e) {
    expect(e.toString()).toEqual('Error: Request failed with status code 404');
  }
})

asyncawait.resolves或结合使用.rejects

test('fetchData2 resolves', async () => {
  await expect(fetchData2()).resolves.toMatchObject({
    data: {
      success: true
    }
  })
})

test('fetchData2 rejects', async () => {
  await expect(fetchData2()).rejects.toThrow()
})

钩子函数

更多的钩子函数参考官网介绍

钩子函数名 描述
beforeAll 全局的测试用例执行前
afterAll 全局的测试用例执行完毕
beforeEach 每次执行测试用例前
afterEach 每次执行测试用例后
describe 将测试用例进行分组
test.only 对单个测试用例进行调试(实用)
import Counter from './Counter';
describe('Counter 的测试代码', () => {
  let counter = null
  beforeAll(() => {
    console.log('beforeAll 全局执行初始化')
  })
  beforeEach(() => {
    counter = new Counter()
    console.log('beforeEach')
  })
  afterEach(() => {
    console.log('afterEach')
  })
  afterAll(() => {
    console.log('afterAll 所有测试用例执行完毕')
  })
  describe('测试增加相关的代码', () => {
    test('测试 Counter 中的 addOne 方法', () => {
      console.log('测试 Counter 中的 addOne 方法')
      counter.addOne();
      expect(counter.number).toBe(1);
    })
    test('测试 Counter 中的 addTwo 方法', () => {
      console.log('测试 Counter 中的 addTwo 方法')
      counter.addTwo();
      expect(counter.number).toBe(2);
    })
  })
  describe('测试减少相关的代码', () => {
    test('测试 Counter 中的 minusOne 方法', () => {
      console.log('测试 Counter 中的 minusOne 方法')
      counter.minusOne();
      expect(counter.number).toBe(-1);
    })
    test('测试 Counter 中的 minusTwo 方法', () => {
      console.log('测试 Counter 中的 minusTwo 方法')
      counter.minusTwo();
      expect(counter.number).toBe(-2);
    })
  })
})

那么,就很容易看出 beforeEach 这些钩子,是属于describe 的(作用域),我们可以在子的 describe 中也增加 钩子,且它的生效范围为它下的所有的测试用例。【由外到内的触发机制】

Mock

Mock函数允许您通过擦除函数的实际实现,捕获对该函数的调用(以及在这些调用中传递的参数),捕获用实例化的构造函数的实例new以及允许对它们进行测试时配置来测试代码之间的链接。返回值。

作用:

  • 捕获函数的调用和返回结果以及this的调用顺序
  • 可以自由的设置返回结果 (mockReturnValue、mockReturnValueOnce)
  • 改变函数的内部实现

使用Mock功能

export const runCallback = (callback) => {
  callback('abc');
}

测试用例:

test.only('测试 runCallback', () => {
  const func = jest.fn(); // mock 函数 捕获函数的调用和返回结果以及this的调用顺序
  // func.mockReturnValueOnce('hello').mockReturnValueOnce('world').mockReturnValueOnce('!')
  func.mockReturnValue('hello') // 自由的设置返回结果
  runCallback(func);
  runCallback(func);
  runCallback(func);
  // expect(func).toBeCalled();
  expect(func.mock.calls.length).toBe(3)
  expect(func.mock.calls[0]).toEqual(['abc'])
  expect(func.mock).toBeCalledWith('abc') // 每次匹配的都是 'abc'
  console.log(func.mock); 
  // { calls: [ [ 'abc' ], [ 'abc' ], [ 'abc' ] ], // 测试函数的回调值
  // instances: [ undefined, undefined, undefined ], // 是否是实例化的函数
  // invocationCallOrder: [ 1, 2, 3 ], // 调用顺序
  // results: // mock的返回值
  //  [ { type: 'return', value: 'hello' },
  //    { type: 'return', value: 'hello' },
  //    { type: 'return', value: 'hello' } ] }
})

.mock property

export const createObject = (classItem) => {
  new classItem()
}
test.only('测试 createObject', () => {
  const func = jest.fn();
  createObject(func);
  console.log(func.mock);
  // { calls: [ [] ],
  //   instances: [ mockConstructor {} ],
  //   invocationCallOrder: [ 1 ],
  //   results: [ { type: 'return', value: undefined } ] }
})

检测实例化返回的测试,可以使用以下进行检测

// 函数被实例化了一次
expect(someMockFunction.mock.instances.length).toBe(1);
// 此函数的第一个实例化返回的对象
//具有“name”属性,其值设置为“test”
expect(func.mock.instances[0].name).toEqual('test');

mock 模块

假设我们有一个从API提取用户的类。该类使用axios调用API,然后返回data包含所有用户的属性:

export const getData = () => {
  return axios.get('/api/user.json').then(res => res.data)
}

现在,为了测试该方法而无需实际访问API(从而创建缓慢而脆弱的测试),我们可以使用该jest.mock(...)函数自动模拟axios模块。

一旦对模块进行了模拟,我们就可以提供一个mockResolvedValuefor .get,以返回我们要针对测试进行断言的数据。实际上,我们说的是我们要axios.get('/users.json')返回假响应。

import axios from 'axios';
jest.mock('axios'); // jest对axios进行模拟,这样不会真正发送axios请求

test.only('测试 getData', async () => {
  // 改变函数的内部实现
  // axios.get.mockResolvedValue({data: 'hello'});
  axios.get.mockResolvedValueOnce({data: 'hello'}); // 模拟返回数据
  await getData().then(data => {
    expect(data).toBe('hello');
  })
})

mock 实现

在某些情况下,超越指定返回值的功能并完全替换模拟功能的实现是有用的。这可以通过模拟函数jest.fnmockImplementationOnce方法来完成。

test.only('测试 runCallback', () => {
  const func = jest.fn()
  // func.mockImplementation(() => { return 'hello' }) 相当于 jest.fn(() => {return 'hello'})
  // 多个函数调用产生不同的结果时用 mockImplementationOnce
  func.mockImplementationOnce(() => {
    return 'hello'
  })
  func.mockImplementationOnce(() => {
    return 'world'
  })
  runCallback(func);
  runCallback(func);
  console.log(func.mock);
  expect(func.mock.results[0].value).toBe('hello');
  expect(func.mock.results[1].value).toBe('world');
})

对于通常具有链式方法(因此总是需要返回this)的情况,我们提供了一个含糖的API,以.mockReturnThis()函数的形式简化了该过程,该函数也位于所有模拟中:

const myObj = {
  myMethod: jest.fn().mockReturnThis(),
};
// is the same as

const otherObj = {
  myMethod: jest.fn(function () {
    return this;
  }),
};

Mock的更多方法请查看官网

你可能感兴趣的:(前端测试框架Jest入门总结)