前端测试之Jest深刻浅出

1. 为何要作前端测试

首先,我认为前端测试并非全部项目都必须的,由于写测试代码是须要要花费必定时间的,当项目比较简单的时候,花时间写测试代码可能反而会影响开发效率,可是须要指出的是,咱们前端开发过程当中,编写测试代码,有如下这些好处:javascript

  1. 更快的发现bug,让绝大多数bug在开发阶段发现解决,提升产品质量
  2. 比起写注释,单元测试多是更好的选择,经过运行测试代码,观察输入和输出,有时会比注释更能让别人理解你的代码(固然,重要的注释仍是要写的。。。)
  3. 有利于重构,若是一个项目的测试代码写的比较完善,重构过程当中改动时能够迅速的经过测试代码是否经过来检查重构是否正确,大大提升重构效率
  4. 编写测试代码的过程,每每可让咱们深刻思考业务流程,让咱们的代码写的更完善和规范。

2. 什么是TDDBDD

2.1 TDD与单元测试

2.1.1 什么是TDD

所谓TDD(Test Driven Development),即测试驱动开发,简单的来讲就是先编写测试代码,而后以使得全部测试代码都经过为目的,编写逻辑代码,是一种以测试来驱动开发过程的开发模式。html

2.1.2 单元测试

所谓单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证。通俗的讲,在前端,单元能够理解为一个独立的模块文件,单元测试就是对这样一个模块文件的测试。前端

对于一个独立的模块(ES6模块),由于功能相对独立,因此咱们能够首先编写测试代码,而后根据测试代码指导编写逻辑代码。vue

因此提到TDD,这里的测试通常是指单元测试java

2.2 BDD与集成测试

2.2.1 什么是BDD

所谓BDD(Behavior Driven Development),即行为驱动开发,简单的来讲就是先编写业务逻辑代码,而后以使得全部业务逻辑按照预期结果执行为目的,编写测试代码,是一种以用户行为来驱动开发过程的开发模式。node

2.2.2 集成测试

所谓集成测试(Integration Testing),是指对软件中的全部模块按照设计要求进行组装为完整系统后,进行检查和验证。通俗的讲,在前端,集成测试能够理解为对多个模块实现的一个交互完整的交互流程进行测试。react

对于多个模块(ES6模块)组成的系统,须要首先将交互行为完善,才能按照预期行为编写测试代码。webpack

因此提到BDD,这里的测试通常是指集成测试。ios

3. Jest使用---引言部分

3.1 咱们如何写测试代码?

若是咱们以前历来没有接触过测试代码,那让咱们本身来设计测试代码的写法,会是什么样呢?咱们须要让测试代码简单,通俗易懂,好比咱们举个例子以下:git

export function findMax (arr) {
    return Math.max(...arr)
}
复制代码

咱们写了一个很简单的获取数组最大值的函数(你可能以为这样写并不严谨,但咱们为了简单,暂时假设输入是非空数值数组),若是对这个函数写一个测试其正确与否的测试程序,它可能构思是这样的:

我指望 findMax([1, 2, 4, 3]) 的结果是 4
复制代码

进一步转化为英文:

I expect findMax([1, 2, 4, 3]) to be 4
复制代码

用程序性的语言表示,expect做为一个函数,为它传入想要测试的对象(findMax函数),把输出结果也作一层封装toBe(4):

expect(findMax([1, 2, 4, 3])).toBe(4)  // 有内味了
复制代码

更进一步,咱们想要增长一些描述性信息,好比

测试findMax函数,我指望 findMax([1, 2, 4, 3]) 的结果是 4
复制代码

这个时候,咱们能够再作一层封装,定义一个test函数,它有两个参数,第一个参数是一些描述性信息(这里是 测试findMax函数),第二个参数是一个函数,函数里能够执行咱们上面的逻辑,以下:

test('findMax函数输出', () => {
    expect(findMax([1, 2, 4, 3])).toBe(4) // 内味更深了
})
复制代码

3.2 简单的本身实现测试代码

咱们本身能够简单的实现下test函数和expect函数,由于存在链式调用toBe,因此expect函数最终应该返回一个具备toBe方法的对象,以下:

// expect函数
function expect (value) {
    return {
        toBe: (toBeValue) => {
            if (toBeValue === value) {
                console.log('测试经过!')
            } else {
                throw new Error('测试不经过!')
            }
        }
    }
}

// test函数
function test (msg, func) {
    try {
        func()
        console.log(`${msg}测试过程无异常!`)
    } catch (err) {
        console.error(`${msg}测试过程出错!`)
    }
}
复制代码

咱们的测试方法,只是对数字作了简单的测试,实际项目中,须要测试的类型是不少的,这个时候咱们就能够选择一些比较成熟的测试框架。一个简单好用,功能强大的工具就呈如今咱们面前,它就是jest

4. Jest使用---入门部分

4.1 准备工做

咱们这部分的例子主要是为了介绍jest最基本的用法,首先咱们先简单的搭建一下演示环境。

第一步,使用npm init -y(个人node版本是v12.14.1npm版本是v6.13.4)初始化项目

第二步,安装jest npm install --save-dev jest(安装能够参考官网

第三步,运行npx jest --init命令,生成一份jest的配置文件jest.config.js,个人选择以下

第四步,运行npm i babel-jest @babel/core @babel/preset-env -D安装babel,而且配置.babelrc以下

{
  presets: [
    [
      '@babel/preset-env',
      {
        targets: {
          node: 'current',
        },
      },
    ],
  ],
};
复制代码

第五步,根目录下创建src文件夹,新建两个文件basic.jsbasic.test.js

第六步,package.json增长一条命令:

"scripts": {
    "test": "jest"
  },
复制代码

以上六步完成后,咱们的项目结构应该以下图

4.2 最基本的jest用法

接下来咱们采用TDD加单元测试的方式来学习jest基本用法:

首先,在basic.js里定义两个工具函数

// 1. 寻找最大值
export function findMax (arr) {
    
}

// 2. 给定一个整数数组 nums 和一个目标值 target,在该数组中找出和为目标值的那 两个 整数,若是存在,返回true,不然返回false
export function twoSum (nums, target) {

};
复制代码

既然是TDD,咱们首先编写测试代码,在这个过程当中,咱们逐步学习各类jest的基本用法。测试代码在basic.test.js文件中编写:

import { findMax, twoSum } from './basic'

// 指望findMax([2, 6, 3])执行后结果为6
test('findMax([2, 6, 3])', () => {
    expect(findMax([2, 6, 3])).toBe(6)
})

// 指望twoSum([2, 3, 4, 6], 10)执行后结果为true
test('twoSum([2, 3, 4, 6], 10)', () => {
    expect(twoSum([2, 3, 4, 6], 10)).toBe(true)
})
复制代码

从上面代码,咱们能够看到,jest测试代码的写法,和以前咱们本身写的是同样的(固然啦,原本就是模仿jest的),此时咱们运行npm test命令,观察命令行输出以下:

注意我红框里的部分, Expected表明指望函数执行的结果,也就是 toBe里的那个值, Received表明实际执行函数获得的结果,由于咱们尚未编写业务代码,因此 Received都是 undefined,最后显示一共 1个测试文件( Test Suites)和 2条测试代码,它们都测试失败了。

接下来咱们完善basic.js里的逻辑

// 1. 寻找最大值
export function findMax (arr) {
    return Math.max(...arr)
}

// 2. 给定一个整数数组 nums 和一个目标值 target,在该数组中找出和为目标值的那 两个 整数,若是存在,返回true,不然返回false
export function twoSum (nums, target) {
    for (let i = 0; i < nums.length - 1; i++) {
       for (let j = i + 1; j < nums.length; j++) {
           if (nums[i] + nums[j] === target) {
               return true
           }
       } 
    }
    return false
};
复制代码

而后咱们再次运行npm test,获得结果以下

咱们能够看到,全部测试用例都经过了(直观的就是都绿了)。这种首先全部测试用例都没有经过(一片红),随着咱们开发过程的进行,一步步的,最终测试代码都经过(一片绿)的过程,就是 TDD和单元测试的开发过程。

4.3 更多的jest matchers

像是上小节,在expect函数后面跟着的判断结果的toBejest中被称为matcher,咱们这一小节就来介绍另一些经常使用的matchers

4.3.1 toEqual

咱们首先改造下刚刚的twoSum函数,让它返回找到的两个数的索引数组(leetcode第一题)

// 2. 给定一个整数数组 nums 和一个目标值 target,在该数组中找出和为目标值的那 两个 整数,
// 并返回他们的数组下标(假设每种输入只会对应一个答案,数组中同一个元素不能使用两遍)。
export function twoSum (nums, target) {
    for (let i = 0; i < nums.length - 1; i++) {
       for (let j = i + 1; j < nums.length; j++) {
           if (nums[i] + nums[j] === target) {
               return [i, j]
           }
       } 
    }
    return []
};
复制代码

接下来测试代码部分咱们只保留对twoSum函数的测试,并同步修改测试代码

test('twoSum([2, 3, 4, 6], 10)', () => {
    expect(twoSum([2, 3, 4, 6], 10)).toBe([2, 3])
})
复制代码

咱们的指望是函数执行的结果是[2, 3]这样的数组,看起来没问题,运行npm test

咱们发现并无经过测试,这是由于,toBe能够判断基本类型数据,可是对于数组,对象这样的引用类型是没办法判断的,这个时候,咱们就须要使用toEqual

test('twoSum([2, 3, 4, 6], 10)', () => {
    expect(twoSum([2, 3, 4, 6], 10)).toEqual([2, 3])
})
复制代码

改为toEqual以后,测试代码就成功了

4.3.2 判断逻辑真假相关的一些matchers

这部份内容很简单,也比较多,因此直接在代码里注释说明:

test('变量a是否为null', () => {
    const a = null
    expect(a).toBeNull()
})

test('变量a是否为undefined', () => {
    const a = undefined
    expect(a).toBeUndefined()
})

test('变量a是否为defined', () => {
    const a = null
    expect(a).toBeDefined()
})

test('变量a是否为true', () => {
    const a = 1
    expect(a).toBeTruthy()
})

test('变量a是否为false', () => {
    const a = 0
    expect(a).toBeFalsy()
})
复制代码

测试结果以下:

4.3.3 not修饰符

很简单,not就是对matcher的否认

test('test not', () => {
    const temp = 10
    expect(temp).not.toBe(11)
    expect(temp).not.toBeFalsy()
    expect(temp).toBeTruthy()
})
复制代码

测试结果以下:

4.3.4 判断数字相关的一些matchers

这部份内容很简单,也比较多,因此直接在代码里注释说明:

// 判断数num是否大于某个数
test('toBeGreaterThan', () => {
    const num = 10
    expect(num).toBeGreaterThan(7)
})

// 判断数num是否大于等于某个数
test('toBeGreaterThanOrEqual', () => {
    const num = 10
    expect(num).toBeGreaterThanOrEqual(10)
})

// 判断数num是否小于某个数
test('toBeLessThan', () => {
    const num = 10
    expect(num).toBeLessThan(20)
})

// 判断数num是否小于等于某个数
test('toBeLessThanOrEqual', () => {
    const num = 10
    expect(num).toBeLessThanOrEqual(10)
    expect(num).toBeLessThanOrEqual(20)
})
复制代码

测试结果以下:

上面介绍的都是整数判断,十分简单,可是若是是浮点数相关的判断,会不太同样,好比,咱们知道0.1 + 0.2 = 0.3这个式子在数学中没有问题,可是在计算机中,因为精度问题,这个0.1 + 0.2结果若是用toBe结果并非准确的0.3,若是咱们想要判断浮点数的相等,在jest中提供了一个toBeCloseTomatcher能够解决:

test('toBe', () => {
    const sum = 0.1 + 0.2
    expect(sum).toBe(0.3)
})

test('toBeCloseTo', () => {
    const sum = 0.1 + 0.2
    expect(sum).toBeCloseTo(0.3)
})
复制代码

上面的测试结果以下:

4.3.5 字符串匹配toMatch

这个matcher就是用来判断字符串是否和toMatch提供的模式匹配,以下:

// 字符串相关
test('toMatch', () => {
    const str = 'Lebron James'
    expect(str).toMatch(/Ja/)
    expect(str).toMatch('Ja')
})
复制代码

4.3.6 数组,集合相关的matchers

可使用toContain判断数组或者集合是否包含某个元素,使用toHaveLength判断数组的长度,代码以下:

test('Array Set matchers', () => {
    const arr = ['Kobe', 'James', 'Curry']
    const set = new Set(arr)
    expect(arr).toContain('Kobe')
    expect(set).toContain('Curry')
    expect(arr).toHaveLength(3)
})
复制代码

4.3.7 异常相关的matchers

使用toThrow来判断抛出的异常是否符合预期:

function throwError () {
    throw new Error('this is an error!!')
}
test('toThrow', () => {
    expect(throwError).toThrow(/this is an error/)
})
复制代码

5. jest进阶用法

5.1 分组测试与勾子函数

所谓分组测试,核心在于,将不一样的测试进行分组,再结合勾子函数(生命周期函数),完成不一样分组的定制化测试,以知足测试过程重的复杂需求。

咱们首先在src下新建两个文件hook.jshook.test.js,这一部分代码在这两个文件中完成,首先直接给出hook.js代码

// hook.js
export default class Count {
    constructor () {
        this.count = 2
    }
    increase () {
        this.count ++
    }

    decrease () {
        this.count --
    }

    double () {
        this.count *= this.count
    }

    half () {
        this.count /= this.count
    }
} 
复制代码

如今呢,咱们想要对Count类的四个方法单独测试,数据互相不影响,固然咱们能够本身去直接实例化4个对象,不过,jest给了咱们更优雅的写法---分组,咱们使用describe函数分组,以下:

describe('分别测试Count的4个方法', () => {
    test('测试increase', () => {
        
    })
    test('测试decrease', () => {
        
    })
    test('测试double', () => {
        
    })
    test('测试half', () => {
        
    })
})
复制代码

这样咱们就使用describe函数配合test将测试分为了四组,接下来,为了能更好的控制每一个test组,咱们就要用到jest的勾子函数。 咱们这里要介绍的是jest里的四个勾子函数beforeEach,beforeAll,afterEach,afterAll

顾名思义,beforeEach是在每个test函数执行以前,会被调用;afterEach则是在每个test函数执行以后调用;beforeAll是在全部test函数执行以前调用;afterAll则是在全部test函数执行以后调用。咱们能够看下面这个例子:

import Count from "./hook"

describe('分别测试Count的4个方法', () => {
    let count
    beforeAll(() => {
        console.log('before all tests!')
    })

    beforeEach(() => {
        console.log('before each test!')
        count = new Count()
    })

    afterAll(() => {
        console.log('after all tests!')
    })

    afterEach(() => {
        console.log('after each test!')
    })

    test('测试increase', () => {
        count.increase()
        console.log(count.count)
    })
    test('测试decrease', () => {
        count.decrease()
        console.log(count.count)
    })
    test('测试double', () => {
        count.double()
        console.log(count.count)
    })
    test('测试half', () => {
        count.half()
        console.log(count.count)
    })
})
复制代码

输出的结果如图:

能够看到,咱们在每一个test执行以前,beforeEach里面从新实例化了count,因此每一次的count是不一样的。合理的使用勾子函数,咱们能够更好的定制测试。

5.2 异步代码测试之定时器

在咱们前端开发过程当中,因为javascript是单线程的,异步编程是咱们开发人员常常要作的工做,而异步代码也是最容易出错的地方,对异步代码逻辑进行测试,是颇有必要的,这一节将对jest如何进行异步测试,作一个详细的介绍。

5.2.1 从最简单的setTimeout开始

咱们首先新建timeout.js,timeout.test.js文件,timeout.js文件代码很简单:

export default (fn) => {
    setTimeout(() => {
       fn()
    }, 2000)
}
复制代码

咱们如今的目标就是去测试,写的这个函数,是否是会像咱们预期的那样,传入一个函数做为参数(简单为主,没有作参数校验),2s后,执行这个函数。

咱们的测试代码(timeout.test.js)以下:

import timeout from './timeout'

test('测试timer', () => {
    timeout(() => {
        expect(2+2).toBe(4)
    })
})

复制代码

若是咱们运行这段测试代码,必定是会经过的,可是,这真的表明咱们写在timeout里的方法测试经过了吗?咱们在timout.js中打印输出一段文字

export default (fn) => {
    setTimeout(() => {
       fn()
       console.log('this is timeout!')
    }, 2000)
}
复制代码

而后咱们运行测试代码(npm test timeout.test这样只运行一个文件),你会发现,什么打印内容都没有输出:

其实产生这种现象的缘由也很简单, jest在运行测试代码,执行 test方法时,从函数内部第一行执行到最后一行,当执行逻辑走到代码块最后一行时,没有异常就会返回测试成功,这个过程当中 不会去等待异步代码的执行结果,因此咱们这样的测试方法,无论 setTimeout里怎么实现,回调函数里怎么实现,都不会执行回调函数内部的逻辑。

若是咱们须要测试代码在真正执行了定时器里的异步逻辑后,才返回测试结果,咱们须要给test方法的回调函数传入一个done参数,并在test方法内异步执行的代码中调用这个done方法,这样,test方法会等到done所在的代码块内容执行完毕后才返回测试结果:

import timeout from './timeout'

test('测试timer', (done) => {
    timeout(() => {
        expect(2+2).toBe(4)
        done()
    })
})
复制代码

咱们能够看到,增长 done参数以后,获得了预期的结果,打印输出了内容,证实咱们回调函数内的代码执行了。

5.2.2 使用fakeTimers提升测试效率

咱们上一小节介绍了如何去测试写在定时器里异步代码的执行,但这里存在一个问题,好比,咱们的定时器可能须要几十秒才执行内部逻辑(这虽然不多见,主要看业务需求),咱们的测试代码也会好久才会返回结果,这无疑大大的下降了开发测试效率。

jest也考虑到了这一点,让咱们可使用fakeTimers模拟真实的定时器。这个fakeTimers在遇到定时器时,容许咱们当即跳过定时器等待时间,执行内部逻辑,好比,对于刚刚的timeout.test,咱们的测试代码能够作以下改变:

  1. 首先,咱们使用jest.fn()生成一个jest提供的用来测试的函数,这样咱们以后回调函数不须要本身去写一个
  2. 其次,咱们使用jest.useFakeTimers()方法启动fakeTimers
  3. 最后,咱们能够经过jest.advanceTimersByTime()方法,参数传入毫秒时间,jest会当即跳过这个时间值,还能够经过toHaveBeenCalledTimes()这个mathcer来测试函数的调用次数。

完整代码以下:

test('测试timer', () => {
    jest.useFakeTimers()
    const fn = jest.fn()
    timeout(fn)
    // 时间快进2秒
    jest.advanceTimersByTime(2000)
    expect(fn).toHaveBeenCalledTimes(1)
})
复制代码

咱们依然得到了预期的测试结果,注意观察输出结果里 测试timer(12ms)对比以前的 测试timer(2021ms),能够看到,定时器的延迟时间,确实被跳过了,这提升了测试开发效率。

5.2.3 更复杂的定时器场景

通过前面两节的介绍,对于定时器这种异步场景的测试代码编写,实际上咱们已经掌握核心内容,这一节,咱们去探讨一个更为复杂的场景,那就是定时器嵌套。

咱们首先改造timout里的代码以下:

export default (fn) => {
    setTimeout(() => {
       fn()
       console.log('this is timeout outside!')
       setTimeout(() => {
            fn()
           console.log('this is timeout inside!')
       }, 3000)
    }, 2000)
}
复制代码

按照上一小节的写法,咱们的测试代码能够改造为:

test('测试timer', () => {
    jest.useFakeTimers()
    const fn = jest.fn()
    timeout(fn)
    // 时间快进2秒
    jest.advanceTimersByTime(2000)
    expect(fn).toHaveBeenCalledTimes(1)
    // 时间快进3秒
    jest.advanceTimersByTime(3000)
    expect(fn).toHaveBeenCalledTimes(2)
})
复制代码

其实也很简单,就是在第一次2s后,再过3s后执行第二个定时器,此时fn被调用了2次,因此咱们只须要加上最后两行代码就能够了。执行结果以下:

咱们能够看到,两条打印结果都输出了。可是目前的这种实现不是很好,试想一下,若是这里面的定时器嵌套比较多,或者咱们不清楚有几个定时器,就会比较麻烦。jest为这种状况提供了两个有用的方法:

  1. jest.runAllTimers()

这个方法就如同它的名字同样,调用以后,会执行全部定时器,咱们的代码能够改造以下:

test('测试timer', () => {
    jest.useFakeTimers()
    const fn = jest.fn()
    timeout(fn)
    jest.runAllTimers()
    expect(fn).toHaveBeenCalledTimes(2)
})
复制代码

能够看到,两个定时器内部的打印都输出了,并且 jest依旧快速的跳过了定时器等待时间。

  1. jest.runOnlyPendingTimers()

这个方法的意思是,只执行当前正在等待的全部定时器,这个例子中,只有外层定时器是正在等待的,内层定时器只有在外层定时器执行时,才处于等待状态,咱们改造测试代码以下:

test('测试timer', () => {
    jest.useFakeTimers()
    const fn = jest.fn()
    timeout(fn)
    jest.runOnlyPendingTimers()
    expect(fn).toHaveBeenCalledTimes(1)
})
复制代码

能够看到,只有外层定时器里的内容被打印输出了。若是咱们想要继续输出内部定时器的内容,由于此时内部定时器处于等待状态,因此再次执行 jest.runOnlyPendingTimers()便可。

关于上述内容,有一点须要说明:

若是咱们编写了多个test函数,它们都使用fakeTimers,必定要在beforeEach勾子中每次都调用jest.useFakeTimers(),不然,多个test函数中的fakeTimers会是同一个,将会互相干扰,产生不符合预期的执行结果

beforeEach(() => {
    jest.useFakeTimers()
})
复制代码

5.3 异步代码测试之数据请求(promise/async await)

5.3.1 传统的promise写法

在咱们前端开发中,经过请求后端接口获取数据是很重要的一个流程,这一节主要就是介绍这个过程当中如何编写测试代码(实际上这里的不少内容,以前介绍定时器的章节是有介绍过的)

为了简单起见,咱们使用axios(npm i axios)这个成熟的库来辅助咱们作数据请求。首先新建request.js, request.test.js这两个文件,在request.js文件请求一个免费api:

import axios from 'axios'

export const request = fn => {
    axios.get('https://jsonplaceholder.typicode.com/todos/1').then(res => {
        fn(res)
        console.log(res)
    })
}
复制代码

咱们在request.test.js中,为了保证异步代码执行完毕后结束测试,和以前介绍的同样,在test的回调函数中传入done参数,在回调函数里执行done(),代码以下:

import { request } from './request'

test('测试request', (done) => {
    request(data => {
        expect(data.data).toEqual({
            "userId": 1,
            "id": 1,
            "title": "delectus aut autem",
            "completed": false
          })
        done()
    })
})
复制代码

咱们能够看到,打印出了请求回来的内容,测试代码也正确的拿到数据,测试经过。

咱们如今改造一下request.js的代码,让它返回一个promise:

export const request = () => {
    return axios.get('https://jsonplaceholder.typicode.com/todos/1')
}
复制代码

为了测试上述代码,咱们request.test.js也要作必定的修改:

test('测试request', () => {
    return request().then(data => {
        expect(data.data).toEqual({
            "userId": 1,
            "id": 1,
            "title": "delectus aut autem",
            "completed": false
          })
    })
})
复制代码

注意,上面的写法不须要传入done参数了,可是,须要咱们使用return返回,若是不写return,那jest执行test函数时,将不会等待promise返回,这样的话,测试结果输出时,then方法将不会执行。咱们能够尝试如下两种写法(改变"completed": true),第一种写法测试不会经过,第二种测试是能够经过的(由于promise并无返回结果):

// 第一种
test('测试request', () => {
    return request().then(data => {
        expect(data.data).toEqual({
            "userId": 1,
            "id": 1,
            "title": "delectus aut autem",
            "completed": true
          })
    })
})
复制代码

// 第二种
test('测试request', () => {
    request().then(data => {
        expect(data.data).toEqual({
            "userId": 1,
            "id": 1,
            "title": "delectus aut autem",
            "completed": true
          })
    })
})
复制代码

上面的测试代码,咱们也能够写成下面的形式:

test('测试request', () => {
    return expect(request()).resolves.toMatchObject({
        data: {
            "userId": 1,
            "id": 1,
            "title": "delectus aut autem",
            "completed": false
          }
    })
})
复制代码

注意,resolves将返回request方法执行后全部返回内容,咱们使用toMatchObject这个matcher,当传入的对象可以匹配到request方法执行后返回对象的一部分键值对,测试就会经过。

5.3.2 使用async await语法糖

async await本质上就是promise链式调用的语法糖,咱们上一小节最后的测试代码,若是使用async await的方式去书写,以下:

// 写法一
test('测试request', async () => {
    const res = await request()
    expect(res.data).toEqual({
        "userId": 1,
        "id": 1,
        "title": "delectus aut autem",
        "completed": false
    })
})
// 写法二
test('测试request', async () => {
    await expect(request()).resolves.toMatchObject({
        data: {
            "userId": 1,
            "id": 1,
            "title": "delectus aut autem",
            "completed": false
            }
        })
})
复制代码

咱们上述两种写法都是能够经过测试的。

5.3.3 对于请求出现错误的测试

在咱们实际项目中,须要对这种接口请求作错误处理,一样,也须要对异常状况编写测试代码。

咱们首先在request.js增长一个方法:

export const requestErr = fn => {
    return axios.get('https://jsonplaceholder.typicode.com/sda')
}
复制代码

这里请求一个不存在的接口地址,会返回404,因而咱们的的测试代码为:

test('测试request 404', () => {
    return requestErr().catch((e) => {
        console.log(e.toString())
        expect(e.toString().indexOf('404') > -1).toBeTruthy()
    })
})
复制代码

这里有个地方须要注意一下,下图是 jest官网的一段说明:

大概意思就是说,若是测试代码里使用 catch,jest不回去执行 catch里的内容,因此须要咱们去写 expect.assertions(1)这句话,表明,指望执行的断言是1次, catch方法算一次断言,因此,正常状况,因为不会执行 catch,这里会报错(执行了0次断言),当这里报错了,说明咱们的代码也按照预期产生了异常。

这种写法目前已经不须要了,详细见removed useless expect.assertions,因此,如今就按照上面那种方式,直接书写,测试经过表明确实如咱们预期的产生异常。

一样的,咱们还可使用另外一种方式完成异常代码测试:

test('测试request 404', () => {
    return expect(requestErr()).rejects.toThrow(/404/)
})
复制代码

这里的rejects和上一节的resolves相互对于,表明执行方法产生的错误对象,这个错误对象抛出404异常(toThrow(/404/))

咱们一样可使用async await语法糖书写异常测试的代码:

test('测试request 404', async () => {
    await expect(requestErr()).rejects.toThrow(/404/)
})
// 或者可使用try catch语句写的更完整
test('测试request 404', async () => {
    try {
        await requestErr()
    } catch (e) {
        expect(e.toString()).toBe('Error: Request failed with status code 404')
    }
})
复制代码

5.4 在测试中模拟(mock)数据

咱们首先新建mock.js, mock.test.js文件

5.4.1 使用jest.fn()模拟函数

首先在mock.js写一个函数:

export const run = fn => {
   return fn('this is run!')
}
复制代码

实际上以前咱们已经使用过jest.fn()了,这里咱们更进一步的学习它。

  1. 首先,咱们的fn()函数能够接受一个函数做为参数,这个函数就是咱们想要jest.fn()为咱们mock的函数,咱们编写mock.test.js
test('测试 jest.fn()', () => {
    const fn = jest.fn(() => {
        return 'this is mock fn 1'
    })
})
复制代码
  1. 其次,jest.fn()能够初始化时候不传入参数,而后经过调用生成的mock函数的mockImplementation或者mockImplementationOnce方法来改变mock函数内容,这两个方法的区别是,mockImplementationOnce只会改变要mock的函数一次:
test('测试 jest.fn()', () => {
    const func = jest.fn()
    func.mockImplementation(() => {
        return 'this is mock fn 1'
    })
    func.mockImplementationOnce(() => {
        return 'this is mock fn 2'
    })
    const a = run(func)
    const b = run(func)
    const c = run(func)
    console.log(a)
    console.log(b)
    console.log(c)
})
复制代码

咱们能够看到,函数执行的结果第一次是 this is mock fn 2,以后都是 this is mock fn 1

一样的,咱们可使用mock函数的mockReturnValuemockReturnValueOnce(一次)方法来改变函数的返回值:

test('测试 jest.fn()', () => {
    const func = jest.fn()
    func.mockImplementation(() => {
        return 'this is mock fn 1'
    })
    func.mockImplementationOnce(() => {
        return 'this is mock fn 2'
    })
    func.mockReturnValue('this is mock fn 3')
    func.mockReturnValueOnce('this is mock fn 4')
        .mockReturnValueOnce('this is mock fn 5')
        .mockReturnValueOnce('this is mock fn 6')
    const a = run(func)
    const b = run(func)
    const c = run(func)
    const d = run(func)
    console.log(a)
    console.log(b)
    console.log(c)
    console.log(d)
})
复制代码

注意到,方法是能够链式调用的,方便屡次输出不一样的返回值。

  1. 最后,咱们可使用toBeCalledWith这个matcher来测试函数的传参数是否符合预期:
test('测试 jest.fn()', () => {
    const func = jest.fn()
    const a = run(func)
    expect(func).toBeCalledWith('this is run!')
})
复制代码

5.4.2 模拟接口中获取的数据

不少时候,咱们在前端开发过程当中,后端接口尚未提供,咱们须要去mock接口返回的数据。

咱们首先在mock.js中编写一个简单的请求数据的代码:

import axios from 'axios'

export const request = fn => {
    return axios.get('https://jsonplaceholder.typicode.com/todos/1')
}
复制代码

接着,咱们在mock.test.js中,使用jest.mock()方法模拟axios,使用mockResolvedValuemockResolvedValueOnce方法模拟返回的数据,一样的,mockResolvedValueOnce方法只会改变一次返回的数据:

import axios from 'axios'
import { request } from './mock'

jest.mock('axios')

test('测试request', async () => {
    axios.get.mockResolvedValueOnce({ data: 'Jordan', position: 'SG' })
    axios.get.mockResolvedValue({ data: 'kobe', position: 'SG' })
    await request().then((res) => {
        expect(res.data).toBe('Jordan')
    })
    await request().then((res) => {
        expect(res.data).toBe('kobe')
    })
})
复制代码

咱们使用jest.mock('axios')来使用jest去模拟axios,测试正确的经过了。

5.5 dom相关测试

dom相关的测试其实很简单,咱们首先新建dom.js, dom.test.js两个文件,代码以下:

// dom.js
export const generateDiv = () => {
    const div = document.createElement('div')
    div.className = 'test-div'
    document.body.appendChild(div)
}

// dom.test.js
import { generateDiv } from './dom'

test('测试dom操做', () => {
    generateDiv()
    generateDiv()
    generateDiv()
    expect(document.getElementsByClassName('test-div').length).toBe(3)
})
复制代码

这里只有一点要说明,jest的运行环境是node.js,这里jest使用jsdom来让咱们能够书写dom操做相关的测试逻辑。

5.6 快照(snapshot)测试

咱们若是没有接触过快照测试,可能会以为这个名字很高大上。因此咱们首先新建snapshot.js, shapshot.test.js来看看快照测试到底是什么。

在咱们的平常开发中,总会写一些配置性的代码,它们大致不会变化,可是也会有小的变动,这样的配置可能以下(snapshot.js):

export const getConfig = () => {
    return {
        server: 'https://demo.com',
        port: '8080'
    }
}
复制代码

咱们的测试代码以下:

import { getConfig } from './snapshot'

test('getConfig测试', () => {
    expect(getConfig()).toEqual({
        server: 'https://demo.com',
        port: '8080'
    })
})
复制代码

这样咱们经过了测试。可是,假如后续咱们的配置改变了,我就须要同步的去修改测试代码,这样会比较麻烦,从而,jest为咱们引入了快照测试,先上测试代码:

test('getConfig测试', () => {
    expect(getConfig()).toMatchSnapshot()
})
复制代码

咱们运行测试代码以后,会在项目根目录下生成一个__snapshots__文件夹,下面有一个snapshot.test.js.snap快照文件,文件内容以下:

// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`getConfig测试 1`] = `
Object {
  "port": "8080",
  "server": "https://demo.com",
}
`;
复制代码

jest会在运行toMatchSnapshot()的时候,首先检查有没有这个快照文件,若是没有,则生成,当咱们改动配置内容时,好比把port改成8090,再次运行测试代码,测试不经过,结果以下:

这个时候,咱们只须要运行 npm test snapshot.test -- -u,就能够自动更新咱们的快照文件,测试再次经过,这就让咱们不须要每次更改配置文件的时候,手动去同步更新测试代码,提升了测试开发效率:

此时咱们的快照文件更新为以下代码:

// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`getConfig测试 1`] = `
Object {
  "port": "8090",
  "server": "https://demo.com",
}
`;
复制代码

6. jest其它一些有用的知识

6.1 让jest监听文件变化

这个功能很简单,咱们只须要运行jest命令的时候,后面加上--watch便可,咱们在package.json中新增一条命令:

"scripts": {
    "test": "jest",
    "test-watch": "jest --watch"
},
复制代码

在新增完这条命令后,为了能让jest能够监听文件变化,咱们还须要把咱们的代码文件变成一个git仓库,jest也正式依靠git的能力实现监听文件变化的,咱们运行git init,接着咱们运行npm run test-watch,在必定时间后,咱们开启监听模式,命令行最后几行输出应该是:

这里对watch模式的几个有用功能作一个简单介绍(也就是图中英文说明):

  1. a键运行全部测试代码
  2. f键只运行全部失败的测试代码
  3. p键按照文件名筛选测试代码(支持正则)
  4. t键按照测试名筛选测试代码(支持正则)
  5. q键盘推出watch模式
  6. enter键触发一次测试运行

这些我建议你们自行去尝试,它们都是十分简单好用的功能。

6.2 生成测试覆盖率文件

测试覆盖率,简单来讲就是咱们业务代码中,编写测试代码的比例,jest给咱们提供了直接生成测试覆盖率文件的方法,也就是运行jest命令时后面加上--coverage参数,咱们修改package.json文件以下:

"scripts": {
    "test": "jest",
    "test-watch": "jest --watch",
    "coverage": "jest --coverage"
},
复制代码

接下来,运行npm run coverage,咱们能够看到命令行输出以下:

这是一份测试覆盖率表格。同时,咱们发现,文件夹下自动生成了一个 coverage文件夹:

咱们在浏览器中运行 index.html,以下图:

这个页面向咱们展现了项目中不一样文件的测试覆盖率,咱们能够点击不一样文件名字进入查看具体一个文件中,哪些代码被测试到了,哪些没有被测试到。

这里对这个表格项目作一个简单的说明:

  1. Statements是语句覆盖率:表示代码中有多少执行的语句被测试到了

  2. Branches是分支覆盖率:表示代码中有多少if else switch分支被测试到了

  3. Functions是函数覆盖率:表示代码中被测试到的函数的占比

  4. Lines是行覆盖率:表示代码中被测试到的行数占比

咱们能够利用生成的测试覆盖率文件,更好的完善改进咱们的测试代码。

6.3 关于jest.config.js配置文件

我对于学习一个工具的配置文件的建议是,首先按照默认的来,当你须要改变配置的时候,再去查阅官方文档学习,不推荐去死记硬背。

我这里也不会去介绍怎么去配置jest文件,咱们能够经过jest初始化时候默认生成的那个jest.config.js来学习(有详细注释),也能够在官网中查阅相关的配置参数。

7. 写在最后

因为篇幅缘由,不适合再介绍更多的信息,更多的api相关的信息,建议去查阅官网来学习。

这篇文章我的认为已经把jest的基础和最核心的内容作了阐述,可能咱们开发过程当中,使用react(enzyme), vue( @vue/test-utils)这样的开发框架,使用webpack这样的工程化工具,在使用jest的时候,会结合使用一些开源库,我相信学好了jest自己以后,配置和使用它们都不会有太多困难。

最后,但愿这篇文章能够帮助到你们,感谢能看到这里的每个小伙伴。

相关文章
相关标签/搜索