Jest学习入门

150 阅读8分钟

1. 安装

使用 yarn 安装 Jest:

yarn add --dev jest

或使用 npm 安装:

npm install --save-dev jest

2. 配置

(目前我是针对Javascript,Typescript做单元测试,暂时不考虑对页面DOM的操作,后期再补这一块)

Jest 将根据你的项目提出一系列问题,并且将创建一个基础配置文件

jest --init

生成jest.config.js

如果使用babel,需要额外依赖

yarn add --dev babel-jest @babel/core @babel/preset-env

然后在根目录创建babel.config.js,来配置Babel使其能够兼容当前的Node版本。


babel.config.js
module.exports = {
presets: [['@babel/preset-env', {targets: {node: 'current'}}]],
};

注意:

Jest版本24 就不再支持Babel 6,推荐使用Babel7

非要Babel 6就使jest固定在23版本

使用Typescript

yarn add --dev @babel/preset-typescript

安装依赖之后,将@babel/preset-typescript添加到babel.config.js中的presets列表中

不过,将 TypeScript 和 Babel 一起使用时有一些 注意事项

由于 Babel 对 Typescript 的支持是通过代码转换(Transpilation)实现的, 而 Jest 在运行时并不会对你的测试用例做类型检查。 如果你需要此功能,可以使用ts-jest,或者单独(或作为构建流程的一部分)直接运行 TypeScript 编译器 tsc 。

额外依赖:

yarn add --dev @types/jest

--watchAll

监控所有文件变化

"scripts": {
"test": "jest --watchAll"
}

--watch

每次只重新测试修改过后的文件

"scripts": {
"test": "jest --watch"
}

3. Matchers

常用的Matchers如下:

toBe
toBeNull
toBeUndefined
toBeDefined
toBeWithinRange
toBeTruthy
toBeFalsy
toBeGreaterThan
toBeGreaterThanOrEqual
toBeLessThan
toBeLessThanOrEqual
toMatch
toContain
toThrow
toBeCloseTo
toHaveLength
toBeInstanceOf
toHaveBeenCalled
toHaveBeenCalledTimes // 被调用的次数
toHaveReturned // function returned
toHaveReturnedTimes // times
toHaveReturnedWith // 函数返回值
arrayContaining
objectContaining
stringContaining

typescript写法:

declare global {
  namespace jest {
    interface Matchers<R> {
      toBeWithinRange(a: number, b: number): R;
    }
  }
}

1. 举第一个例子

toBetoBeNulltoBeUndefinedtoBeDefinedtoBeTruthytoBeFalsytoBeGreaterThantoBeGreaterThantoBeLessThantoBeLessThanOrEqual

我有一个函数correctData用来计算时间总长的,如果计算正常,应该是返回总和为24*60*60*1000的值,对这个函数进行测试:

test.spec.js文件:

import { correctData } from "../src/testFunction";

test('calculate 24', () => {
    let result = correctData(data)
    let duration = 0
    for (const key in result) {
        duration += result[key]
    }
    let final = duration/(1000*60*60) // 计算正常应该是返回final === 24
    expect(final).toBe(24)
    expect(final).not.toBeNull()
    expect(final).not.toBeUndefined()
    expect(final).toBeDefined()
    expect(final === 24).toBeTruthy()
    expect(final !== 24).toBeFalsy()
    expect(final).toBeGreaterThan(20)
    expect(final).toBeGreaterThanOrEqual(24)
    expect(final).toBeLessThan(48)
    expect(final).toBeLessThanOrEqual(24)
})

这些匹配器看例子就懂什么意思了

在控制台跑一下

image.png

简单的例子passed!

2. 举第二个例子

toBeCloseTo,toMatch, toContain, toHaveLength, toBeInstanceOf

test('other Matchers', () => {
    let m = 0.1+0.2
    let arr = [1,2,3,4,5]
    expect(m).toBeCloseTo(0.3) // 首先0.1+0.2在js里肯定不等于0.3,只能无限接近
    expect(m.toString()).toMatch('0.3')
    expect(arr).toContain(4)
    expect(arr).toHaveLength(5)
    expect(arr).toBeInstanceOf(Array) // 通过原型判断
})

3. 举第三个例子

toHaveBeenCalled

这个就比较有用了,这个用来确保函数被调用

test.js

export function callFn(callback){
    if(callback) {
        callback()
    }
}
import { callFn } from "../src/test";

test('Matchers——toHaveBeenCalled', () => {
    let fn = jest.fn()
    callFn(fn)
    expect(fn).toHaveBeenCalled();
})

image.png

4. 举第四个例子

toThrow测试函数被调用时的抛出情况

testFunction.js

export function fnThrow(){
    try {
        let result = axios.get('http://xxx.xxx.xxx.xxx/api/model/v1/s') 
        // 这里是错误的url
    } catch (e) {
        throw new Error('someting wrong');
    }
}

testFunction.spec.js

import { fnThrow } from "../src/testFunction";
test('throw Matchers', () => {
    expect(() => fnThrow()).toThrow()
    expect(() => fnThrow()).toThrow(Error)
    expect(() => fnThrow()).toThrow('someting wrong')
})

看一下测试结果

image.png

简单的匹配器懂了,就看看怎么使用了

4. 异步代码

第一种:

使用done()方法
使用done(),测试用例会一直执行到done结束

第二种:

如果返回是Promise对象,可以直接使用return

如果promise 请求成功肯定不会走catch,但是测试会依然通过

  // 因此需要加上下面一句,指定必须只能执行一次expect
  expect.assertions(1)

第三种:

如果返回的是一个Promise对象,可以直接使用return + resolves/rejects写法
.resolves和.rejects 可以将promise的值返回,方便直接链式调用匹配器

第四种:

如果返回的是一个 Promise 对象,async + await

Jest的钩子函数

  • beforeAll:所有测试之前执行
  • afterAll:所有测试执行完之后
  • beforeEach:每个测试实例之前执行
  • afterEach:每个测试实例完成之后执行

5. Mock函数

jest.fn(),jest.mock()和jest.spyOn()

jest.fn()

这是创建Mock最简单的方式,如果没有定义函数内部的实现,jest.fn()会返回undefined作为返回值

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

jest.mock()

封装的请求方法可能在我们在其他模块被调用的时候,并不需要进行实际的请求 可以使用jest.mock()去mock整个模块

jest.spyOn()

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

方法:

// 使用模拟函数实例化
mockFn.mock.instances
// 清除所有信息
mockFn.mockClear()
// 执行所有mockClear操作,并删除任何模拟返回值
mockFn.mockReset()
// 恢复原始实现
mockFn.mockRestore()
// 接受一个函数
mockFn.mockImplementation(fn)
// 接受一个函数执行一次
mockFn.mockImplementationOnce(fn)
// 函数名
mockFn.mockName(value)
//
mockFn.mockReturnThis()
// 返回值
mockFn.mockReturnValue(value)
// 返回值,执行一次
mockFn.mockReturnValueOnce(value)
// 
mockFn.mockResolvedValue(value)
// 
mockFn.mockResolvedValueOnce(value)
// 
mockFn.mockRejectedValue(value)
// 
mockFn.mockRejectedValueOnce(value)

mockFn.mockReturnThis() 语法糖:

jest.fn(function () {
return this;
});

6. 例子

tesxAxios.js


import axios from "axios";

export function httpTest() {
    return axios.get('http://192.168.1.125/api/model/v1/things')
}

export function eventTest() {
    return axios.get('http://192.168.1.125/api/event/v1/events?filter=%7B%22thingId%22%3A%221c84e0271f9a4edea6cf9df9330c94b8%22%2C%22typeId%22%3A%22main.FX3U80M%22%7D&select=severity%2C10301%2C10323%2C10314%2C10315%2C10316%2C10373%2C10374%2C10309%2C10318%2C10319%2C10320%2C10321%2C10322%2C10339%2C10341%2C10343%2C10334%2C10335%2C10325%2C10326%2C10327%2C10328%2C10369%2C10370%2C10371%2C10372%2C10336%2C10384')
}

export function wrongHttpTest() {
    return axios.get('http://192.168.1.125/api/model/v1/thingss')
}

export function hasPost(param) {
    return axios.post('http://192.168.1.125/api/event/v1/events', {...param})
}

export function httpPromiseTest() {
    return new Promise((resolve) => {
        resolve('succ')
    })
}

testAxios.spec.js


import {httpTest, wrongHttpTest, hasPost, eventTest, httpPromiseTest } from "../src/testAxios";

// http get获取数据(获取数据不为空)
test('http get方法', (done) => {
    httpTest().then(res => {
        expect(res.data).not.toBe(null)
    })
    done()  // 如果不加 done,还没执行到 .then 方法,测试用例已经结束了

})

// http get 404
test('http get方法404',  (done) => {
    //写法一
    // wrongHttpTest().catch((e)=>{
    //     expect(e.response.status.toString()).toMatch('404')
    //     done()
    // })
    //写法二
    expect(wrongHttpTest()).rejects.toMatch('404')
})

test('http post', async(done) => {
    const service = {
        "typeId": "main.Q60EHCPU",
        "_time": "2021-12-13T00:21:56.797Z",
        "thingId": "807bd74656d94a70aaf5cfda16f27333",
        "M3000": true,
        "severity": 1
    };
    try {
        const before = await eventTest()
        let count = before.data.events.length
        await hasPost(service)
        setTimeout(async function (){
            const after = await eventTest()
            expect(after.data.events.length).toBe(count+1)
        }, 2500)
        // done()
    } catch (err) {
        // done(err)
    }
});

test('http promise', () =>{
    return httpPromiseTest().then(res => {
        expect(res).toMatch('succ')
    })
})

test('http promise catch', () =>{
    expect.assertions(1) // 加上就会报错了
    return httpPromiseTest().catch(e => {
        expect(e).toMatch('Error')
    })
})

testMork.js

import axios from "axios";

export default {
    async getdata(){
        return axios.get('http://192.168.1.125/api/model/v1/things').then(data =>{
            console.log('fetch Data')
            return data
        })
    }
}

testMork.spec.js


import axios from "axios";
import testMork from '../src/testMork'
jest.mock('axios')
// jest.mork('../src/control.js')

test('测试jest.fn()调用', () => {
    let mockFn = jest.fn();
    // let mockFn = jest.fn().mockName('getdata')
    let result = mockFn('hello');
    // 断言mockFn的执行后返回undefined
    expect(result).toBeUndefined();
    // 断言mockFn被调用
    expect(mockFn).toBeCalled();
    // 断言mockFn被调用了一次
    expect(mockFn).toBeCalledTimes(3);
    // 断言mockFn传入的参数为1, 2, 3
    expect(mockFn).toHaveBeenCalledWith('hello');
})

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

test('mock', async () => {
    axios.get.mockResolvedValue({
        data: '456'
    })
    expect(await testMork.getdata()).toStrictEqual({
        data: '456'
    });

})

test('jest.spyOn', async() => {
    expect.assertions(1);
    const spyFn = jest.spyOn(testMork);
    await testMork.getdata();
    expect(spyFn).toHaveBeenCalled();
})

testFunction.js

export function correctData (data) {
    let lastRunState = data.service.lastTrueTime === '0001-01-01T00:00:00Z' ?
        0 : Date.parse(data.service.lastTrueTime);
    let lastWaitState = data.stop.lastTrueTime === '0001-01-01T00:00:00Z' ?
        0 : Date.parse(data.stop.lastTrueTime);
    let nowLastState = Math.max(lastRunState, lastWaitState)
    let offlineTime = Date.parse(data.end) - nowLastState
    let alramTime = data.alarm.lastValue ? data.alarm.trueDuration - offlineTime : data.alarm.trueDuration
    let waitTime
    let runTime
    let timeRange = Date.parse(data.end) - Date.parse(data.start)
    if (lastRunState > lastWaitState) {
        waitTime = data.stop.trueDuration
        runTime = timeRange - offlineTime - data.stop.trueDuration
    } else {
        runTime = data.service.trueDuration
        waitTime = timeRange - offlineTime - data.service.trueDuration
    }
    let countRun = (runTime / (waitTime+runTime))*alramTime
    let countWait = (waitTime / (waitTime+runTime))*alramTime
    waitTime = waitTime - countWait
    runTime = runTime - countRun
    return {
        service: runTime,
        stop: waitTime,
        offline: offlineTime,
        alarm: alramTime
    }
}
export function fnThrow(){
    try {
        let result = axios.get('http://192.168.1.125/api/model/v1/thingss')
    } catch (e) {
        throw new Error('someting wrong');
    }
}
export function callFn(callback){
    if(callback) {
        callback()
    }
}

testFunction.spec.js

import { correctData, fnThrow, callFn } from "../src/testFunction";

let data = {
        "alarm":{
            "falseCnt":4,
            "falseDuration":12600000,
            "firstFalseTime":"2021-12-09T02:00:00Z",
            "firstTrueTime":"2021-12-09T00:00:00Z",
            "lastFalseTime":"2021-12-09T06:00:00Z",
            "lastTrueTime":"2021-12-09T06:36:00Z",
            "lastValue":true,
            "trueCnt":6,
            "trueDuration":73800000
        },
        "end":"2021-12-09T16:00:00Z",
        "offline":{
            "falseCnt":0,
            "falseDuration":-83010264511686,
            "firstFalseTime":"0001-01-01T00:00:00Z",
            "firstTrueTime":"0001-01-01T00:00:00Z",
            "lastFalseTime":"0001-01-01T00:00:00Z",
            "lastTrueTime":"0001-01-01T00:00:00Z",
            "lastValue":false,
            "trueCnt":0,
            "trueDuration":83010348331686
        },
        "service":{
            "falseCnt":5,
            "falseDuration":14700000,
            "firstFalseTime":"2021-12-09T01:00:00Z",
            "firstTrueTime":"2021-12-09T00:00:00Z",
            "lastFalseTime":"2021-12-09T06:30:00Z",
            "lastTrueTime":"2021-12-09T06:36:00Z",
            "lastValue":true,
            "trueCnt":5,
            "trueDuration":71700000
        },
        "start":"2021-12-08T16:00:00Z",
        "stop":{
            "falseCnt":5,
            "falseDuration":71700000,
            "firstFalseTime":"2021-12-09T00:00:00Z",
            "firstTrueTime":"2021-12-09T01:00:00Z",
            "lastFalseTime":"2021-12-09T06:36:00Z",
            "lastTrueTime":"2021-12-09T06:30:00Z",
            "lastValue":false,
            "trueCnt":5,
            "trueDuration":14700000
        }
    }

test('calculate 24', () => {
    let result = correctData(data)
    let duration = 0
    for (const key in result) {
        duration += result[key]
    }
    let final = duration/(1000*60*60)
    expect(final).toBe(24)
    expect(final).not.toBeNull()
    expect(final).not.toBeUndefined()
    expect(final).toBeDefined()
    expect(final === 24).toBeTruthy()
    expect(final !== 24).toBeFalsy()
    expect(final).toBeGreaterThan(20)
    expect(final).toBeGreaterThanOrEqual(24)
    expect(final).toBeLessThan(48)
    expect(final).toBeLessThanOrEqual(24)
    expect(final).toBeWithinRange(1,25)
})
test('throw Matchers', () => {
    expect(() => fnThrow()).toThrow()
    expect(() => fnThrow()).toThrow(Error)
    expect(() => fnThrow()).toThrow('someting wrong')
})
test('other Matchers', () => {
    let m = 0.1+0.2
    let arr = [1,2,3,4,5]
    expect(m).toBeCloseTo(0.3)
    expect(m.toString()).toMatch('0.3')
    expect(arr).toContain(4)
    expect(arr).toHaveLength(5)
    expect(arr).toBeInstanceOf(Array)
})
test('Matchers——toHaveBeenCalled', () => {
    let fn = jest.fn(() =>{})
    callFn(fn)
    expect(fn).toHaveBeenCalled();
})