Jest单元测试进阶

  Jest 命令行窗口中的指令

  在学习Jest单元测试入门的时候,给Jest命令提供了一个参数 --watchAll, 让它监听测试文件或测试文件引入的文件的变化,从而时时进行测试。但这样做也带来一个问题,只要改变一点内容,Jest就会把所有的测试都跑一遍,有点浪费资源。有没有可能对--watchAll模式进行进一步的优化?在命令窗口中执行npm run test 看一看就知道了, 测试完成后,你会发现还有很多提示(Watch Usage),这些就是对--watchAll模式的优化

   Press f to run only failed tests.  按f 键,只测试以前失败的测试。执行npm run test 的时候,发现有一个测试失败了,这时我们只想测试这个失败的测试,可以按f了。演示一下,随便把一个测试用例改为错误,比如 把request 的mock 改为name: 'jason'

jest.mock('request', () => {
    return (url, callback) => {
        callback(null, 'ok', {name: 'jason'})
    }
});

   测试重新跑了一遍了(watchAll 模式),命令窗口中显示了错误, 并且在最下面显示press w to show more,  同时光标在闪烁,等待输入。此时按w,  显示了上图中的内容,再按f, 只跑了失败的测试,因为三个测试skipped 了, 当然肯定还是有错误,因为我们还没有修改测试代码

   修改测试代码到正确并保存,它只跑了刚才失败的测试。但此时你再修改func.test.js 文件或其它测试用例,发现测试不会再运行了,显示No failed test found,按f键退出

  因为f模式是测试以前,就是上一次,失败的测试,我们已经修改好了上一次失败的测试,所以它就不会再进行测试了。按f, 重新回到了watchAll 模式。

  总结一下,f 模式的使用就是,npm run  test 有失败测试,按f,  修改失败到成功,再按f 退出该模式。

  Press o to only run tests related to changed files.  按o ,只会去测试和当前被改变文件相关的测试。但这时,你按o,发现报错了。为什么呢?因为让Jest 去测试改变文件中的测试,但Jest它自己并不知道哪个文件发生了变化,Jest本身,不具备比较文件改变的功能,那怎么办?需要借助git. 因为git 就是追踪文件变化的,只要把工作区和仓库区的代码一对比,就知道哪个文件发生变化了。因此需要把项目变成git 项目。在根目录下,先建.gitignore 文件,再执行git init, 把项目变成git 项目,否则会把node_modules 放到 git 仓库中。为了演示,把fetchData的测试从func.test.js拆分为出来,就叫fetchData.test.js

jest.mock('request', () => {
    return (url, callback) => {
        callback(null, 'ok', {name: 'sam'})
    }
});

const fetchData = require('./func').fetchData;

test('should return data when fetchData request success', () => {
   return fetchData().then(res => {
       expect(res).toEqual({name: 'sam'})
   })
})

  git add . and git commit -m "init git" 把文件提交到git 仓库。执行npm run test 启动测试,按o 进入到o 模式, 可以看到如下提示,没有文件发生变化。

   这时更改一个文件,如forEach 加一个空行,看一下控制台,只有func.test.js 测试文件执行了,其它测试文件并没有执行, 再改一个fetchData.test.js 文件,两个测试文件执行了,还是只跑改变文件中的测试。这时让我想起了jest 命令的另一个参数,--watch,  o 模式 不就是--watch 吗。把package.json 中的 --watchAll 改成 --watch 

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

  重新启动npm run test, 有了-a模式,run all the tests, 这不就是--watchAll,  原来 --watch,  --watchAll,  a 模式,o 模式,是这样互通的。

  再看一下p,  按照文件名执行测试,我们提供一个文件名,它只会对该文件进行测试,可以使用正则表达式来匹配文件名。按p,  提示输入pattern,  再输入fetch,  它就会用fetch 去匹配所有的测试文件名,找到了fetchData.test.js 测试文件,然后它就执行了。如果找不到任何测 试文件,它什么测试都不会执行。

  t 则是匹配的test 名字,每一个test 都有一个描述,这个描述可以称之为test 的名字。提供一个test 的名字,它只跑这个test,用法和p 一样。

  q 退出watch, enter 就是跑一次单元测试,无论是在什么模式下,只要按enter,就会跑一次对应模式的测试。

  Jest 快照测试

  传统的单元测试通常都是,执行代码,断言比较,看看是不是和预期的效果一致。但这种断言测试在某些情况下不太合适。比如配置文件,配置文件本来就是放在那里,不用执行代码,如果进行比较断言,就是它和它本身进行比较,肯定相等,这种测试不是太好。之所以对配置文件进行测试,就是希望它不要被随便改了。如果改了,要通知到同事。还有就是UI,它就长这样,不用断言了,我们更希望UI 做好以后,不要随便改了,确定要改了,那就通知大家。对于这两种不希望被改的场景,更好的测试办法,就是把它保存起来,以后每次测试的时候,就和保存的内容进行对比,看有没有改变。把某一时刻的内容保存起来,就是形成了快照,快照就是用来保存的某一时刻的状态。Jest的快照测试也是如此,保存与对比。它提供了一个toMatchSnapshot() 方法,当第一次运行测试的时候,没有快照,那就把这一时刻的状态保存起来,形成快照。以后再运行测试的时候,它会生成一个新的那一时该的快照与以前的快照进行比较,如果两者一致,就表明内容没有变化,测试通过,如果两者不一致了,就表示内容发生改变,jest 就会报错了。新建config.js,

export const axiosConfig = {
    devUrl: 'local',
    productUrl: '/',
    header: {
        'accet': 'json'
    }
}

 config.test.js 写一个快照测试

import { axiosConfig } from './config';

test('axios config', () => {
    expect(axiosConfig).toMatchSnapshot();
})

  npm run test, 这时项目根目录下生成了一个文件夹__snapshots__,下面有一个config.test.js.snap, 这就是生成的快照文件,它的名字和测试文件的名字一致。打开看一看,它就是把文件内容用字符串的形式保存起来了。这时不管运行多少次npm run test, 测试都会通过,因为config.js 没有改。改变一下,比如加一下id

export const axiosConfig = {
    devUrl: 'local',
    productUrl: '/',
    header: {
        'accet': 'json'
    },
    id: '23'
}

  这时jest 报错了,如果确定要改成这样,那就需要更新快照了。命令行中按w,多了u和i,  u就是表示更新失败快照,i 则表示交互式的更新快照。 此时按u, 测试重跑,更新成功。那什么是交互式的更新快照呢?它是针对多个失败的快照而言的,按i,你可以一个一个进行快照的确认和更新,如果按u,则是一次性全部更新所有快照,可能不是你想要的结果。如果两个快照失败了,一个要更新,一个不要更新,那u就无能为例了,只能使用i。在config.js再写一个配置

export const fetchConfig = {
    method: 'post',
    time: '2019'
}

  那测试文件中,再写一个快照测试

test('fetch config', () => {
    expect(fetchConfig).toMatchSnapshot();
})

  这时测试肯定没有问题,同时改一下两个配置文件,id改了'24', time改为'2019/11', 2个快照测试都失败了。此时按i, 你会发现只显示一个快照失败,表示这次只确认这一个失败的快照,它也提示了u更新快照,s 跳过这个测试,q退出该交互模式,如果更新,按u,此时又显示了一个失败的快照,再按u,更新完毕。这就是交互式,一个一个的更新,更为灵活。

  但有的时候,time是动态生成的,比如fetchConfig中的time 改成new Date(), 每一次跑单元测试的时候,time 都不一样,jest肯定报错。这时可以给toMatchSnapshot() 方法传递一个参数{ time: expect.any(Date) 表示什么时间都可以,就不要匹配时间了。

test('fetch config', () => {
    expect(fetchConfig).toMatchSnapshot({
       time: expect.any(Date)
    });
})

  Jest Manual Mock

  在以前mock 函数的时候,我们都会把mock 函数的实现放到测试文件中。manual mock 则是创建一个文件夹__mocks__, 把所有mock 函数的实现放到该文件夹下,不过这里要注意 __mocks__ 文件夹的位置,你要mock 哪个文件中的函数,__mocks__文件夹就要和哪个文件放到同一级目录中。新建__mocks__文件夹之后,再在其下面新建一个和要mock的文件的同名文件,在这个文件中就可以写函数的实现。 比如我们要mock func.js 中fetchData, 那就要在func.js 同一级目录(根目录)中新建__mocks__, 然后在其下面建func.js 文件件,在func.js 中就可以mock fetchData。

 

  当在测试文件中,jest.mock(./func.js),然后引入fetchData 时,jest 自动会到__mocks__ 目录中找func.js 文件,取里面的fetchData 函数,这就是mock的函数了。fetchData.test.js

jest.mock('./func');
const fetchData= require('./func').fetchData;

test('should return data when fetchData request success', () => {
   return fetchData().then(res => {
       expect(res).toEqual({name: 'sam'})
   })
})

  这种mock 有一个问题,在fetchData.test.js 里面测试 一个add. 

const add = require('./func').add;
test('add', () => {
    let sum = add(3, 2);
    expect(sum).toBe(5)
 })

  测试报错,add is not a function.  这是因为jest.mock('./func.js');  整个func.js 模块被mock了,require('./func').add 的时候,它是从mock 的func.js 模块中,就是__mocks__

文件下的func.js 里面去找add, 很显然,没有,所以报错了。怎么解决,不使用require了,使用jest.requireActual(), 字面意思,require真实的,就是从真实的模块,而不是从mock 的模块中引入。 jest.requireActual('./func').add, 从真的func.js中引入add.

const add = jest.requireActual('./func').add;

   有mock 就有unmock(), 取消mock。

jest.unmock('./func.js');

  如果你mock 的是node_modules 第三方模块,那就要在根目录(node_modules同级目录)新建__mocks__ 文件夹,然后在其下面新建和要mock 模块同名的文件句,如mock request.js 模块,使用request.js 的时候,我们使用的是require('request'), 所以就可以在__mocks__ 文件中建一个request.js.

// 自动mock 这个模块(request) 所有暴露出来的方示
jest.genMockFromModule('request');
let request = require('request');
request = jest.fn((url, fn) => {
    fn('error', 'body', {name: 'sam'});
} )
module.exports = request;

  当mock的node_modules中的模块时,jest 是自动mock, 执行测试的时候,如果看到你require 第三方模块,它自动会从__mocks__文件夹中找这个模块,肯定是mock的模块。

  mock timer

  它主要是针对定时器setTimeout, setTimeinterval 提出的。比如在代码中有一个函数需要3s 之后执行,那么在测试的时候,就要在3s以后,测试函数有没有执行,有点浪费时间

function lazy(fn) {
    setTimeout(() => {
        fn();
    }, 3000);
}

test('should call fn after 3s', (done) => {
    const callback = jest.fn();
    lazy(callback);
    setTimeout(() => {
        expect(callback).toBeCalled();
        done()
    }, 3001);
})

  所以jest 提供了mock timer 的功能,不要再使用真实的时间在这里等了,一个假的时间模拟一下就可以了。首先是jest.useFakeTimers() 的调用,它就告诉jest 在以后的测试中,可以使用假时间。当然只用它还不行,因为它只是表示可以使用,我们还要告诉jest在哪个地方使用,当jest 在测试的时候,到这个地方,它就自动使用假时间。两个函数,jest.runAllTimers(), 它表示把所有时间都跑完。jest.advanceTimer() 快进几秒。具体到我们这个测试,我们希望执完lazy(callback) 就调用, 把lazy函数中的3s时间立刻跑完。可以使用jest.runAllTimers();

jest.useFakeTimers(); // 可以使用假函数

test('should call fn after 3s', () => {
    const callback = jest.fn();
    lazy(callback);
    jest.runAllTimers(); // 在这里,把lazy函数里面的3s立即执行完
    expect(callback).toBeCalledTimes(1);
})

  但如果我们的lazy 函数中有两个setTimeout 函数,runAllTimers 就会有问题,因为它把所有时间都跑完了,不管有几个setTimeout. 把lazy 函数改为如下

function lazy(fn) {
    setTimeout(() => {
        fn();
        setTimeout(() => {
            fn();
        }, 2000);
    }, 3000);
}

  你会发现fn 被调用了两次。但有时,只想测试最外层的setTimeout有没有被调用,这时就要用jest.advanceTimersByTime(3000)

test('should call fn after 3s', () => {
    const callback = jest.fn();
    lazy(callback);
    jest.advanceTimersByTime(3000); // 快进3秒
    expect(callback).toBeCalledTimes(1);
})

  没有问题,如果再想测试内层的setTimout 有没有被调用,再快进就好了,不过要注意快进的时间,2s, 因为它会在上一个advanceTimerByTime的时间基础上进行快进

test('should call fn after 3s', () => {
    const callback = jest.fn();
    lazy(callback);
    jest.advanceTimersByTime(3000); // 快进3秒
    expect(callback).toBeCalledTimes(1);
    jest.advanceTimersByTime(2000); // 再快进2秒
    expect(callback).toBeCalledTimes(2);
})

  现在你会发现,如果在一开始的时候,直接快进5s,它的效果就和runAlltimers 一样了。最后一个问题,就是多个测试中都使用advanceTimersByTime,因为它是累加时间的,第二个测试的advanceTimersByTime的时间肯定会在第一个测试中的advanceTimersByTime 时间上相加。解决办法是beforeEach(). 在beforeEach 中调用jest.useFackTimers,每次测试之前,先初始化timer,把timer归零

beforeEach(() => {
    jest.useFakeTimers(); // 可以使用假函数
})

test('should call fn after 3s', () => {
    const callback = jest.fn();
    lazy(callback);
    jest.advanceTimersByTime(3000); // 快进3秒
    expect(callback).toBeCalledTimes(1);
    jest.advanceTimersByTime(2000); // 再快进2秒
    expect(callback).toBeCalledTimes(2);
})

test('should call fn after 3s', () => {
    const callback = jest.fn();
    lazy(callback);
    jest.advanceTimersByTime(3000); // 快进3秒
    expect(callback).toBeCalledTimes(1);
    jest.advanceTimersByTime(2000); // 再快进2秒
    expect(callback).toBeCalledTimes(2);
})

 

posted @ 2019-11-09 18:35  SamWeb  阅读(4842)  评论(2编辑  收藏  举报