Jest 前端测试框架的使用

42 阅读9分钟

Jest 介绍

Jest是Facebook出品的一个JavaScript测试框架,相对其他测试框架其最大的特点是内置了常用的测试工具开箱即用,比如零配置、自带断言、测试覆盖率工具等功能。

它适用于使用以下项目:Babel、TypeScript、Node、React、Angular、Vue等等!

Jest 主要特点:

  • 零配置
  • 自带断言
  • 快照测试功能,通过对比UI代码生成的快照文件,实现对React等常见框架的自动车测试
  • Jest 测试用例是并行执行的,而且只执行发生改变的文件所对应的测试,提升测试速度。
  • 测试覆盖率工具
  • Mock 模拟

安装

npm i jest @types/jest -D

生成配置文件:

npx jest --init

点击查看配置文件中,配置项说明文档

默认运行所有测试

jest

仅运行指定文件名称或文件路径的测试

# 指定测试文件的名称
jest my-test
# 指定测试文件的路径
jest path/to/my-test.js

仅运行在 hg/git 上有改动但尚未提交的文件

jest -o

仅运行与 path/to/fileA.js 和 path/to/fileB.js相关的测试

jest --findRelatedTests path/to/fileA.js path/to/fileB.js

仅运行匹配特定名称的测试用例(主要是匹配 describe 或 test 的名称)

jest -t name-of-spec

监视模式

运行监视模式

jest --watch # 默认执行 jest -o 监视有改动的测试
jest --watchAll # 监视所有测试

watchAll 模式

监视文件的更改并在任何更改时重新运行所有测试。 若果你是想要重新运行仅基于已更改文件的测试,那么请使用 --watch 选项。

watch 模式

该模式下可以指定名称或路径来监视特定的测试文件 监视文件更改,并重新运行与已更改的文件相关的测试。 当文件发生更改时,如果你想要重新运行所有测试,可以使用 --watchAll 选项。

注意 watchAll 模式是需要Git支持的,它监视Git仓库中的文件更改,并重新运行已更改的文件相关测试。

监听模式中的辅助命令

在监听模式中,我看可以看到一下指令提示:

image.png

  • a 运行所有测试用例
  • f 检测运行错误的测试用例
  • o 仅运行测试修改过的文件
  • p 以文件名正则表达式模式进行测试文件的过滤。(会根据文件名与绝对路径来进行匹配)
  • t 根据测试的名称(即test函数的第一个参数)来过滤
  • q 退出监听模式
  • Enter 重新运行测试

要退出以上命令进入的过滤模式,只需要按提示按 w 再按 c 清除即可。

使用ES6模块

npm i babel-jest @babel/core @babel/preset-env -D
// babel.config.js
module.exports = {
    presets:[
        ['@babel/preset-ent',{
            targets:{
                node:'current'
            }
        }]
    ]
}

Jest 使用

常用匹配器

  • toBe 匹配值(string、number、boolean)

最简单测试一个值的方法是使用精确匹配的方法。

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

在上面的代码中,expect (2 + 2) 返回了一个"预期"的对象。 预期的值为4(toBe(4)) 运行Jest时,它会跟踪所有失败的用例,打印出错误日志。

如果要检测对象的值,则使用 toEqual

test('对象赋值', () => {
  const data = {one: 1};
  data['two'] = 2;
  expect(data).toEqual({one: 1, two: 2});
});

还可以使用 not 取反

test('不等于0', () => {
    expect(2 + 2).not.toBe(0);
});

真值

代码中的undefinednullfalse有不同含义,若你在测试时不想区分他们,可以用真值判断。

  • 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(n).toBeFalsy();
  expect(z).toBeFalsy();
  // 以上匹配结果均通过
});

数值

大多数的比较数字有等价的匹配器。

toBeGreaterThan 大于 toBeGreaterThanOrEqual 大于等于 toBeLessThan 小于 toBeLessThanOrEqual 小于等于 toBeCloseTo

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 和 toEqual 一样可以进行数值比较
  expect(value).toBe(4);
  expect(value).toEqual(4);
});

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

test('两个浮点数字相加', () => {
      const value = 0.1 + 0.2;

      //  这句会报错,因为浮点数有舍入误差
      //expect(value).toBe(0.3);     

      expect(value).toBeCloseTo(0.3); // 这句可以运行
    });
});

字符串

您可以检查对具有 toMatch 正则表达式的字符串︰

test('there is no I in team', () => {
  expect('team').not.toMatch(/I/);
});

test('but there is a "stop" in Christoph', () => {
  expect('Christoph').toMatch(/stop/);
});

数组和可迭代对象

你可以通过 toContain来检查一个数组或可迭代对象是否包含某个特定项:

const shoppingList = [  'diapers',  'kleenex',  'trash bags',  'paper towels',  'milk',];

test('shoppingList数组中包含milk', () => {
  expect(shoppingList).toContain('milk');
  expect(new Set(shoppingList)).toContain('milk');
});

异常

若你想测试某函数在调用时是否抛出了错误,你需要使用 toThrow

function compileAndroidCode() {
  throw new Error('you are using the wrong JDK!');
}

test('compiling android goes as expected', () => {
  expect(() => compileAndroidCode()).toThrow();
  expect(() => compileAndroidCode()).toThrow(Error);

  // 您还可以使用必须包含在错误消息中的字符串或regexp
  expect(() => compileAndroidCode()).toThrow('you are using the wrong JDK');
  expect(() => compileAndroidCode()).toThrow(/JDK/);

  // 或者,您可以使用下面这样的regexp来匹配一个确切的错误消息
  expect(() => compileAndroidCode()).toThrow(/^you are using the wrong JDK$/); // Test fails
  expect(() => compileAndroidCode()).toThrow(/^you are using the wrong JDK!$/); // Test pass
});

匹配器的完整列表,请查阅 参考文档

测试异步代码

Promise

测试返回一个Promise,则Jest会等待Promise的resove状态 如果 Promise 的状态变为 rejected, 测试将会失败。

例如,有一个名为fetchData的Promise, 假设它会返回内容为'peanut butter'的字符串 我们可以使用下面的测试代码︰

test('the data is peanut butter', () => {  
    return fetchData().then(data => {  
        expect(data).toBe('peanut butter');  
    });  
});

Async/Await

或者,您可以在测试中使用 async 和 await。 写异步测试用例时,可以在传递给test的函数前面加上async。 例如,可以用来测试相同的 fetchData 方案︰

test('the data is peanut butter', async () => {
  const data = await fetchData();
  expect(data).toBe('peanut butter');
});

test('the fetch fails with an error', async () => {
  expect.assertions(1);
  try {
    await fetchData();
  } catch (e) {
    expect(e).toMatch('error');
  }
});

你也可以将 async and await和 .resolves or .rejects一起使用。

test('the data is peanut butter', async () => {
  await expect(fetchData()).resolves.toBe('peanut butter');
});

test('the fetch fails with an error', async () => {
  await expect(fetchData()).rejects.toMatch('error');
});

上述示例中,async and await实际上是一种基于Promise的异步语法糖。

如果是使用类似 settimeout 的方式:

const getdata = (call)=>{
    setTimeout(()=>{
        call({ val: 111 })
    },3000)
}

// 错误示例,Jest并不会等待setTimeout 3s的时间
// test('异步数据', ()=>{
//     getdata((data)=>{
//         expect(data).toEqual({ val: 111 })
//     })
// })

// 正确做法是传入done调用
test('异步数据', (done)=>{
    getdata((data)=>{
        done()
        expect(data).toEqual({ val: 111 })
    })
})

.resolves / .rejects

您还可以使用 .resolves 匹配器在您期望的声明,Jest 会等待这一 Promise 来解决。 如果 Promise 被拒绝,则测试将自动失败。

test('the data is peanut butter', () => {
  return expect(fetchData()).resolves.toBe('peanut butter');
});

一定不要忘记把整个断言作为返回值返回⸺ return语句的话,在 fetchData 返回的这个 promise 变更为 resolved 状态、then() 有机会执行之前,测试就已经被视为已经完成了。

如果你希望Promise返回rejected,你需要使用 .rejects 匹配器。 它和 .resolves 匹配器是一样的使用方式。 如果 Promise 被拒绝,则测试将自动失败。

test('the fetch fails with an error', () => {
  return expect(fetchData()).rejects.toMatch('error');
});

上述几种异步形式没有优劣之分,你可以在你的项目或者文件中混合或单独使用他们。 这只取决于哪种形式更能使您的测试变得简单。

定时器模拟 - Mock

  • useFakeTimers - 启用假定时器代替setTimeout
  • useRealTimers - 恢复使用原始定时器
  • runAllTimers - 快进使得所有定时器执行结束
  • runOnlyPendingTimers - 快进使得当前进行的定时器执行结束(不管其他的定时器),常用于解决递归造成的无限定时器循环
  • advancertimersbytime - 指定快进的时间

使用以上定时器模拟,可以避免测试settimeout是的等待时间,可以控制提前结束定时器获得结算结果。

以下使用实例:

const getdata = (call)=>{
    setTimeout(()=>{
        call({ val: 111 })
        setTimeout(()=>{
            console.log(1234)
        },1000)
    },3000)
}

// 启用假定时器
jest.useFakeTimers()

// 示例1 ----- useRealTimers
test('异步数据', ()=>{
    getdata((data)=>{
        expect(data).toEqual({ val: 111 })
    })
    // 快进所有定时器结束
    jest.runAllTimers();
})

// 示例2 ----- runOnlyPendingTimers
jest.useFakeTimers()
test('异步数据', ()=>{
    getdata((data)=>{
        expect(data).toEqual({ val: 111 })
    })
    // 结束当前进行的定时器,因此getdata中的内层(第二个定时器不会再执行)
    jest.runOnlyPendingTimers();
})

// 示例3 ----- advancertimersbytime
test('异步数据', ()=>{
    getdata((data)=>{
        expect(data).toEqual({ val: 111 })
    })
    // 指定快进2s
    jest.advancertimersbytime(2000);
    // 再次指定快进1s,此时总共快进了3s,所以getdata中的第一个定时器会完成执行。如果再次快进1s,则内部第二个定时器也会完成执行
    jest.advancertimersbytime(1000);
})

模拟函数 - Mock

假设我们要测试函数 forEach 的内部实现,这个函数为传入的数组中的每个元素调用一次回调函数。

export function forEach(items, callback) {
  for (let index = 0; index < items.length; index++) {
    callback(items[index]);
  }
}

当我们想校验函数执行的次数是否正确时,就可以使用 mock函数 jest.fn,mock函数调用执行后会存在以下运行结果,其中calls中储存了函数回调所接受的参数并以数组的形式储存。results是mockFn函数执行的返回结果。

test('forEach 测试校验',()=>{
    const arr = [1,2,3]
    const mockFn =  jest.fn((val ,ii)=>{
        return val * 10
    })
    .mockName('forEach') // 指定mockFn的名称,当mockFn内部报错时,提示错误的名称
    
    forEach(arr, mockFn)

    // 校验执行次数是否正确
    expect(mockFn.mock.calls.length).toBe(arr.length)

    console.log(mockFn.mock)
})

image.png

模拟模块 - Axios请求

假定有个从 API 获取用户的类。 该类用 axios 调用 API 然后返回 data,其中包含所有用户的属性:

import axios from 'axios'
import { getUserInfo } from './user'

// 模拟模块
jest.mock('axios')

test('获取用户信息接口测试', async () => {
    const userInfo = { name: '张三' }
    const resData = { data:userInfo }

    // mock 响应结果
    axios.get.mockResolvedValue(resData)

    const resultData = await getUserInfo()
       
    expect(resultData.data).toEqual(userInfo)
})

模拟实现 - mock

当你需要根据别的模块定义默认的Mock函数实现时,mockImplementation 方法是非常有用的。

    // foo.js
    module.exports = function () {
      // some implementation;
    };

    // test.js
    jest.mock('../foo'); // this happens automatically with automocking
    const foo = require('../foo');

    // foo is a mock function
    foo.mockImplementation(() => 42);
    foo();
    // > 42

当你需要模拟某个函数调用返回不同结果时,请使用 mockImplementationOnce 方法︰

const myMockFn = jest
  .fn()
  .mockImplementationOnce(cb => cb(null, true))
  .mockImplementationOnce(cb => cb(null, false));

myMockFn((err, val) => console.log(val));
// > true

myMockFn((err, val) => console.log(val));
// > false

当 mockImplementationOne定义的实现逐个调用完毕时, 如果定义了jest.fn ,它将使用 jest.fn 

const myMockFn = jest
  .fn(() => 'default')
  .mockImplementationOnce(() => 'first call')
  .mockImplementationOnce(() => 'second call');

console.log(myMockFn(), myMockFn(), myMockFn(), myMockFn());
// > 'first call', 'second call', 'default', 'default'