Jest 单元测试入门

  首先是为什么要写单元测试? 主要还是测试我们代码有没有达到预期的效果,其次,如果严格按照TDD(测试驱动开发)来进行开发的话,我们还会更加注重产品细节,代码可能更加健壮。因为TDD是测试放到第一位,写代码之前,先写测试。测试怎么写?肯定是思考产品的各种使用场景,以及在每种场景下,会有什么效果或异常,思考好了,测试写完了,整个产品也就是一清二楚了。真正写代码的时候,只是把测试场景用代码实现,这些所谓的场景就是一个个测试用例。

  其次是怎么写单元测试。测试的目的就是看看代码有没有达到我们的预期,那肯定是先引入要测试的代码,然后是运行代码,最后再和我们的预期做比较,如果和预期一致,就表明代码没有问题,如果没有达到预期, 就是代码有问题。和预期进行比较,就是断言。这也就是意味着,每一个测试中最终都会落脚到一个断言。如果没有断言(比较),测试也没有意义,代码运行完了,结果呢?不知道。断言就是比较,判断正不正确,对不对,1 + 1 是不是等于2, 就是一个最简单的断言,1+1是计算,程序的运行,2就是期望或预期,等于就是判断或比较,具体到测试代码(jest)中,就是expect(1+1).toBe(2). 在测试程序中, 只要看到expect 什么, to 什么,这就是断言,还有可能是assert.

  最后是运行测试代码。单元测试写了,肯定要运行,不运行,怎么知道结果。这里是我学习时最大的困惑,想明白了,有些问题也就迎刃而解了。当我们运行单元测试,就是在命令行中输入jest的时候,实际上是在node 环境下执行js代码。使用jest 进行单元测试,就是启动了一个node程序来执行测试代码。每当遇到测试问题的时候,先想一想这个前提,说不定,问题就解决了。

  好了,说的也差不多了,那就一起来学习一下Jest 吧。Jest有一个好处,就是不用配置也能用,开箱即用,它提供了断言,函数的mock等常用的测试功能。npm install jest --save-dev, 安装jest 之后,就可以使用它进行单元测试了。打开git bash,输入mkdir jest-test && cd jest-test && npm init -y, 新建jest-test 项目并初始化为node项目。再npm install jest --save-dev, 安装jest ,现在就可以进行测试了。先从简单的函数测试开始学起。touch func.js, 新建一个func.js 文件,暴漏一个greeting 函数,注意使用commonJs 的格式,因为jest 是在node环境下运行的,node暂时没有实现ES6 module

function greeting(guest) {
    return `Hello ${guest}`;
}
 module.exports = greeting;

   函数写完了,那怎么测试呢,测试代码放到什么地方呢?Jest识别三种测试文件,以.test.js结尾的文件,以.spec.js结尾的文件,和放到__tests__ 文件夹中的文件。Jest 在进行测试的时候,它会在整个项目进行查找,只要碰到这三种文件它都会执行。干脆,再写两个函数,用三种测试文件分别进行测试, func.js 如下

function greeting(guest) {
    return `Hello ${guest}`;
}

function createObj(name, age) {
    return {
        name,
        age
    }
}

function isTrueOrFasle(bool) {
    return bool
}

module.exports = {
    greeting,
    createObj,
    isTrueOrFasle
}

  新建greeting.test.js测试greeting 函数,createObj.spec.js来测试createObj函数,新建一个__tests__ 文件夹,在里面建一个isTrue.js 来测试isTrueOrFalse 函数。 具体到测试代码的书写,jest 也有多种方式,可以直接在测试文件中写一个个的test或it用来测试,也可以使用describe 函数,创建一个测试集,再在describe里面写test或it , 在jest中,it和test 是一样的功能,它们都接受两个参数,第一个是字符串,对这个测试进行描述,需要什么条件,达到什么效果。第二个是函数,函数体就是真正的测试代码,jest 要执行的代码。来写一下greeting.test.js 文件,greeting 函数的作用就是 传入guest参数,返回Hello guest.  那对应的一个测试用例就是 传入sam,返回Hello sam.  那描述就可以这么写, should return Hello sam when call  greeting with param sam, 具体到测试代码,引入greeting 函数,调用greeting 函数,传入‘sam’ 参数, 作一个断言,函数调用的返回值是不是等于Hello sam.  greeting.test.js 如下

const greeting = require('./fun').greeting;

test('should return Hello sam when input sam', () => {
    let result = greeting('sam');
    expect(result).toBe('Hello sam');
})

  这和文章开始说的一样,测试的写法为三步,引入测试内容,运行测试内容,最后做一个断言进行比较,是否达到预期。Jest中的断言使用expect, 它接受一个参数,就是运行测试内容的结果,返回一个对象,这个对象来调用匹配器(toBe) ,匹配器的参数就是我们的预期结果,这样就可以对结果和预期进行对比了,也就可以判断对不对了。按照greeting测试的写法,再写一下createObj的测试,使用it

const createObj = require('./fun').createObj;

it('should return {name: "sam", age: 30} when input "sam" and 30', () => {
    let result = createObj('sam', 30);
    expect(result).toEqual({name: 'sam', age: 30}); // 使用toEqual
})

  最后是isTrueOrFalse函数的测试,这里最好用describe(). 因为这个测试分为两种情况,一个it 或test搞不定。对一个功能进行测试,但它分为多种情况,需要多个test, 最好使用descibe() 把多个test 包起来,形成一组测试。只有这一组都测试完成之后,才能说明这个功能是好的。它的语法和test 的一致,第一个参数也是字符串,对这一组测试进行描述, 第二个参数是一个函数,函数体就是一个个的test 测试。

const isTrueOrFasle = require('../fun').isTrueOrFasle;

describe('true or false', () => {
    
    it('should return true when input true', () => {
        let result = isTrueOrFasle(true);
        expect(result).toBeTruthy();  // toBeTruthy 匹配器
    })

    test('should return false when input fasle', () => {
        let result = isTrueOrFasle(false);
        expect(result).toBeFalsy();  // toBeFalsy 匹配器
    })
})

  三个测试写完了,那就运行一下,看看对不对。把package.json中的scripts 的test 字段的值改成 'jest', 然后npm run test 进行测试, 可以看到三个测试都通过了。 修改一下,让一个测试不通过,比如isTrue.js中把第一个改成false,

  it('should return true when input true', () => {
        let result = isTrueOrFasle(false);
        expect(result).toBeTruthy();  // toBeTruthy 匹配器
    })

  再运行npm run test ,

   可以看到失败了,也指出了失败的地方,再看一下它的描述,它把组测试放到前面,后面是一个测试用例的描述,这样,我们就很轻松看到哪一个功能出问题了,并且是哪一个case. 这也是把同一个功能的多个test case 放到一起的好处。

   我们再把它改回去,再执行npm run test,如果这样改动测试,每一次都要执行测试的时候,使用npm run test就有点麻烦了,jest 提供了一个watchAll 参数,会对测试文件以及测试文件引用的源文件进行实时监听,如果有变化,立即进行测试。package.json中的 test 改成成jest --watchAll

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

  npm run test, 就可以启动jest 的实时测试了。当然你也可以随时停止掉,按q 键就可以。

  jest 的基本测试差不多了,再来看看它的异步代码的测试, 先把所有的测试文件删掉,再新建一个func.test.js 文件,现在就只有func.js 和 func.test.js 了。处理异步或是用回调函数, 或是promise 。

  回调函数

  最常见的回调函数就是ajax请求,返回数据后执行成功或失败的回调。在Node 环境下,有一个npm 包request, 它可以发送异步请求,返回数据后调用回调函数进行处理,npm i request --save, 安装一下,然后func.js 修改如下

const request = require('request');

function fetchData(callback) {
  request('https://jsonplaceholder.typicode.com/todos/1', function (error, response, body) {
    callback(body);
  });
}

module.exports = fetchData;

  那怎么测试?肯定调用fetchData, 那就先要创建一个回调函数传给它,因为fetchData获取到数据后,会调用回调函数,那就可以在回调函数中创建一个断言,判断返回的数据是不是和期望的一样。func.test.js 文件修改为如下测试代码。

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

test('should return data when fetchData request success', () => {
    function callback(data) {
        expect(data).toEqual({
            "userId": 1,
            "id": 1,
            "title": "delectus aut autem",
            "completed": false
        })
    }

    fetchData(callback);
})

  执行npm run test 命令,看到pass,但其实并没有达到预期的效果,在callback 函数中加入console.log(data) 试一试,就知道了。测试文件改变后,jest 会重新跑一遍(开启了watch),但并没有打印出data,也就是说callback 函数并没有被调用。这是为什么,我在这个地方想了好久,主要还是对异步了解的不够。当执行jest 测试的时候,实际上是node 执行test函数体的代码,首先看到callback的函数声明,它声明函数,然后看到fetchData() ,它就调用这个函数,请求https://jsonplaceholder.typicode.com/todos/1 接口,这个时候,getTodo函数就执行完了。你可能会想,回调函数都没有执行,这个函数怎么算执行完了呢?回调函数并不是代码执行的,而是放到node的异步队列中被执行的。异步的请求,可以看作是一个对话,

  执行fetchData: " hi, node, 你帮我执行一个请求,如果请求成功,就执行这个回调函数"

  node: "好,我帮你请求” 

  然后node 就请求了,然后实时监听请求的状态,如果返回数据,它就把回调函数插入到它的异步队列中。Node的事件循环机制,就把这个函数执行了。

  这时再看异步函数,其实,异步函数的作用,只是一个告知的作用,告知环境来帮我做事情,只要告知了,函数就算执行完了,其它剩下的事情,请求接口,执行回调函数,就是环境的事了。

  只要一告知,getTodo 函数就执行完了,继续向下执行,由于函数的执行是该测试的最后一行代码,它执行完之后,这个测试就执行完了,没有错误,jest 就pass了. 但是该测试并没有覆盖到callback函数的调用,实际上在背后,node是帮我们发送请求,执行callback 的。这也就是官网说的,By default, Jest tests complete once they reach the end of their execution. That means this test will not work as intended: The problem is that the test will complete as soon as fetchData completes, before ever calling the callback. 那怎么办,官方的建议是使用done. 就是test的第二个参数接受一个done, 然后在callback 里面加done(),  如下所示

test('should return data when fetchData request success', (done) => {
    function callback(data) {
        console.log(data);
        expect(data).toEqual({
            "userId": 1,
            "id": 1,
            "title": "delectus aut autem",
            "completed": false
        })
        done();
    }

    fetchData(callback);
});

   测试又重新跑了一遍,还是报错了,不过是断言写错了,表明callback 调用了,达到了预期的效果。data 是一个字符串,toEqual了一个对象,所以测试失败了。Json parse 一下data 就可以了。

expect(JSON.parse(data)).toEqual({
            "userId": 1,
            "id": 1,
            "title": "delectus aut autem",
            "completed": false
        })

  测试通过了。思考一下done 的作用,加上done 表示等待,jest 在执行单元测试的时候,如果它看到一个测试用例(test) 的第二个函数参数有一个done参数, 它就知道,这个测试用例,执行完最后一行代码的时候,还不算测试完成,还要等待,等待done函数的执行,只有done函数执行了,这个测试用例才算完成,才能执行下一个test. 如果done函数没有调用,它就不能执行下一个测试(test)。加上done 以后,一个测试有没有执行完的依据就不是执行到最后一行代码,而是看done() 有没有被调用。也就是说,done 改变了测试的默认行为,因为默认情况下,只要执行到一个测试的最后一行代码,就认为个测试执行完了,就要执行下一个测试,但有了done就不一样了,它执行完这一行代码后,并不会继续执行下一个测试,而是等待,等待done() 函数的执行,只有done()函数执行了,它才会执行下一个测试。具体到这个测试用例,当jest执行到fetchData(callback) 这一行代码的时候,他就暂停执行了,这时node 就会在背后发送请求,请求成功后,把回调函数放到异步队列中,node的事件循环机制就会执行这个回调函数,而回调函数中正好有done(), done() 函数执行了,这时jest 看到done函数执行了,就把测试置为pass, 然后执行下一个测试。当然jest也不会一直等着,默认是5s,如果5s后done 还没有执行,它就执行下一个测试,这也表明测试失败了。有时,网落太慢或没有网的时候, 你再跑这个测试,你会现如下错误

   这就是5s内没有调用 done() 测试失败的例子。

  总结一下,异步回调函数的测试,一个是使用done作为参数,一个是调用done,在测试的某个地方一定要触发或者调用done()。done是针对一个个test测试用例而言的,目的就是告诉jest一个个test 测试真正完成依据是什么。如果一个测试有done参数,就表示这个测试完成的依据是done的调用,执行到测试的最后一行代码,也不算完事,只有done调用了,才算完事,没有办法,jest在执行这个测试的时候,就只能等待done的调用,只有调用了,它才会执行一个测试。如果一个测试没有done参数,那么这个测试的完成的依据就是执行完最后一行代码,执行完最后一行代码,jest就可以执行下一个测试了。  当然也不会一直等待,默认是5s。

  Promise

  Promise 相对好测试一点,因为promise 可以使用then的链式调用。只要等待它的resolve, 然后调用then 来接受返回的数据进行对比就可以了,如果没有resolve 肯定是失败了。等待resolve,在测试中是使用的return, return Promise 的调用,就是等待它的resolve. 把fetchData 函数转化成使用promise 进行请求,func.js如下

const request = require('request');

function fetchData() {
  return new Promise((resolve, reject) => {
    request('https://jsonplaceholder.typicode.com/todos/1', function (error, response, body) {
      if (error) {
        reject(error);
      }
      resolve(body);
    });
  })
}
module.exports = fetchData;

  测试函数(func.test.js)改为

test('should return data when fetchData request success', () => {
    return fetchData().then(data => {
        expect(JSON.parse(data)).toEqual({
            "userId": 1,
            "id": 1,
            "title": "delectus aut autem",
            "completed": false
        })
    })
});

   进行promise测试的时候,在测试代码中,一定要注意使用return, 如果没有return,就没有等待,没有等待,就没有resolve,then 也就不会执行了,测试效果达不到。如果你想测试error, 把测试代码改成error ,

test('should return err when fetchData request error', () => {   
    return fetchData()
        .catch(e => {
            expect(e).toBe('error')
        })
});

  jest 显示pass, 但这个error 的测试并没有执行,因为 fetchData 返回数据了,没有机会执行catch error。按理说,这种情况要显示fail,表示没有执行到。怎么办,官网建议使用expect.assertions(1); 在测试代码之前,添加expect.assertions(1);

test('should return err when fetchData request error', () => {   
    expect.assertions(1); // 测试代码之前添加

    return fetchData()
        .catch(e => {
            expect(e).toBe('error')
        })
});

  这时jest 显示fail 了。expect.assertions(1); 就是明确告诉jest, 在执行这个测试用例的时候,一定要做一次断言。后面的数字是几,就表明在一个test中,一定要做几次断言,如果没有执行catch,也就没有执行断言,和这里的1,要做一次断言不符,测试失败,也就达到了测试的目的。

  对于promise的测试,还有一个简单的方法,因为promise 只有两种情况,一个是fullfill, 一个是reject,expect() 方法返回的对象提供了resolves 和rejects 属性,返回的就是resolve和reject的值,可以直接调用toEqual等匹配器。看一下代码就知道了

test('should return data when fetchData request success', () => {
return expect(fetchData()).resolves.toMatch(/userId/); // 直接在expect函数中调用 fetchData 函数 }); test('should return err when fetchData request error', () => { return expect(fetchData()).rejects.toBe('error'); });

  除了使用then的链式调用,还可以用async/await 对promise 进行测试,因为 await后面的表达式就是promise. 这时test的函数参数之前要加上async 关键字了。

test('should return data when fetchData request success', async () => {
    let expectResult = {
        "userId": 1,
        "id": 1,
        "title": "delectus aut autem",
        "completed": false
    };

    let data = await fetchData();
    expect(JSON.parse(data)).toBe(expectResult);  // 直接在expect函数中调用 fetchData 函数
});


test('should return err when fetchData request error', async () => {   
    expect.assertions(1);
    try {
        await fetchData();
    } catch (e) {
        expect(e).toBe('error');
    }
});

  当然,也可以把async/await 与resolves 和rejects 相结合,

test('should return data when fetchData request success', async () => {
    await expect(fetchData()).resolves.toMatch(/userId/);  // 直接在expect函数中调用 fetchData 函数
});


test('should return err when fetchData request error', async () => {   
    await expect(fetchData()).rejects.toBe('error');
});

  Mock 函数

  有时你会发现,进行单元测试时,要测试的内容依赖其他内容,比如上面的异步请求,依赖网络,很可能造成测试达不到效果。 能不能把依赖变成可控的内容?这就用到Mock。Mock就是把依赖替换成我们可控的内容,实现测试的内容和它的依赖项隔离。那怎么才能实现mock呢?使用Mock 函数。在jest中,当我们谈论Mock的时候,其实谈论的就是使用Mock 函数代替依赖。Mock函数就是一个虚拟的或假的函数,所以对它来说,最重要的就是实现依赖的全部功能,从而起到替换的作用。通常,mock函数会提供以下三个功能,来实现替换

  函数的调用捕获,设置函数返回值,改变原函数的实现。

  怎样创建Mock 函数呢?在jest 创建一个Mock 函数最简单的方法就是调用jest.fn() 方法。

const mockFunc = jest.fn();

  函数的调用捕获指的是这个函数有没有被调用,调用的参数是什么,返回值是什么,通常用于测试回调函数,mock 真实的回调函数。就像官网举的forEach函数,它接受一个回调函数,每个调用者都会传递不同的回调函数过来,我们事先并不知道回调函数,再者我们测试forEach 的重点是,该函数是不是把数组中的每一项都传递给回调函数了,所以只要是一个函数就可以了,但该函数必须把调用的信息都保存下来,这就是Mock 函数的调用捕获,为此mock 函数还有一个mock 属性。在func.test.js 中,声明forEach, 然后写一个test测试,测试中就使用jest.fn() 生成的mock 函数来mock 真实的回调函数。

const forEach = (array, callback) => {
    for (let index = 0; index < array.length; index++) {
        const element = array[index];
        callback(element);
    }
}

test('should call callback when forEach', () => {
    const mockFun = jest.fn(); // mock 函数
    const array = [1, 2];

    forEach(array, mockFun); // 用mock函数代替真实的回调函数

    console.log(mockFun.mock);
    expect(mockFun.mock.calls.length).toBe(2)
})

  也打印出来mock函数mockFun的mock 属性

 

   calls 保存的就是调用状态,results保存的就是返回值。calls 是一个数组,每一次的调用都组成数组的一个元素,在这里调用了两次,就有两个元素。每一个元素又是一个数组,它则表示的是函数调用时的参数,因为每次的调用都传递了一个参数给函数,所以数组只有一项。如果有多个参数,数组就有多项,按照函数中的参数列表依次排列。这时候,就可以做断言,函数调用了几次,就判断calls.length. expect(mockFun.mock.calls.length).toBe(2) 就是断言函数是不是调用了两次。expcet(mockFun.mock.calls[0][0]) .toBe(1)就是断言第一次调用的时候传递的参数是不是1. 可能觉得麻烦了, 的确有点麻烦了,幸好,jest 对函数的mock参数进行了简单的封装,提供了简单的匹配器, toHaveBeenCalled(),  toHaveBeenCalledTimes() ,使用起来有点方便了。你可能见过toBeCalled(),  其实,它和toHaveBeenCalled() 功能是一模一样的,使用哪个都行。

   函数返回值。有的时候,你不想调用函数,直接获取到函数的返回值就可以了,比如异步函数, 以fetchData 为例,它直接返回一个promise 就好了,根本没有必要请求服务器。mock函数有mockReturnValue(),  它的参数就是返回值。不过它不能返回promise. 可以使用mockResolvedValue直接返回promise的值. 对fetchData 进行mock, 然后设置它的mockResolvedValue()

const fetchData= jest.fn();
// fetchData.mockReturnValue("bar");
fetchData.mockResolvedValue({name:'sam'});

  当我们调用fetchData函数, 它就会返回{name: 'sam'}.  但这时又会发现另外一个问题,fetchData 是从外部组件引入来的,无法在func.test.js 中直接mock. 我们要先引入fetchData,测试fetchData 的时候,就可以这么写。引入fetchData, 然后让fetchData = jest.fn() 进行mock , 然后使用mockResolvedValue ()设置返回值, fetchData.mockResolvedValue ({name: 'sam'}), fetchData测试如下

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

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

  引入fetchData并mock, 又叫mock module, mock 一个模块。 当然这也只是一种实现方式, 引入一个模块,然后对这个模块暴露出来函数依次进行mock, 当我们测试的时候,调用模块暴露的函数就变成了调用mock函数,这也相当于mock了整个模块。但是如果一个模块暴露出很多函数,那么引入并mock的方式,就有点麻烦了。比如func.js  再暴露出三个简单 的方法。

const request = require('request');
exports.fetchData = function fetchData() {
  return new Promise((resolve, reject) => {
    request('https://jsonplaceholder.typicode.com/todos/1', function (error, response, body) {
      resolve(body);
    });
  })
}

exports.add = (a, b) => a + b;
exports.subtract = (a, b) => a -b;
exports.multiply = (a, b) => a * b;

  引入并mock 的方式就变成了

let func = require('./func');
func.add = jest.fn();
func.subtract = jest.fn();
func.fetchData = jest.fu();
func.multiply = jest.fu();

  有点麻烦,不过jest 提供了一个mock()方法,第一个参数是要mock的模块,可以自动mock这个模块。自动mock这个模块是什么意思呢?就是把模块暴露出来的方法,全部自动转换成mock函数jest.fn().(automatically set all exports of a module to the Mock Function). jest.mock('./func'), 就相当于把func.js 变成了

exports.fetchData = jest.fn();
exports.add = jest.fn(); exports.subtract = jest.fn(); exports.multiply = jest.fn();

  不用每个函数都单独mock,方便多了。当我们再require('./func.js')的时候,就require 这个mock 函数了, 测试函数就变成了如下内容,不过要注意,先mock 模块,再require引入。

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

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

  改变函数实现。 有时你不想使用默认的mock函数jest.fn(),尤其是测试回调函数的时候,你想提供回调函数实现,比如上面的forEach, 确实写一个真实的回调函数进行测试,心里更有底一点。mock 函数实现也有两种方法,jest.fn() 可以接受一个参数,这个参数就可以是一个函数实现。forEach 中的mock 函数就可以成

const mockFun = jest.fn(x => x + 1);

  再调mockFun的时候, 实际上调的是x => x + 1; 函数。forEach 的测试修改一个

test('should call callback when forEach', () => {
    const mockFun = jest.fn(x => x + 1);
    const array = [1, 2];

    forEach(array, mockFun); // 用mock函数代替真实的回调函数

    console.log(mockFun.mock);
    expect(mockFun.mock.calls.length).toBe(2)
})

  可以看到有返回值了,测试正是调用了x =>x +1 函数实现

  但是当我们使用jest.mock() 来mock一个模块的时候,jest 已经把所有的方法自动mock成jest.fn(),无法给它传参,也就无法提供实现了。这就要用到第二种mock实现方法了,mock 函数提供了一个方法mockImplementation(), 它的参数也是一个函数实现,使用mockImplementation() 来mock fetchData,让它返回{name: 'sam'}

  
 fetchData.mockImplementation(() => {
       return Promise.resolve({name: 'sam'})
  })

   fetchData的整个测试

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

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

  以上jest.mock() 是mock 自己的module,  第三方模块比如request, 要怎么mock啊? 方法是一样,就是mock 的第一个参数要改一下,直接写要mock 的第三方模块名,它会从node modules 里面去找,然后自动mock. jest.mock('request'), request 模块暴露出来的就是jest.fn(). 如果不确定jest.mock 第三方模块的时候,发生了什么,我们可以先mock, 再require, 最后console.log 一下,还是拿request 为例,按照三步走,代码如下,

jest.mock('request');
const request = require('request');
console.log(request);

  可以看到不光整个模块被mock了,就连里面的方法也被mock了。这时再使用request, 就是使用的mock的 request了,mock 函数的所有用法都是可以使用了,比如按照request 真实的使用方法,提供一个实现。现在就可以换一种思路来mock fetchData,由于fetchData调用request,我们mock request, fetchData就不用mock了。在test 文件mock request 并提供实现,这时fetchData调用的就是mock的request了。

jest.mock('request');
const request = require('request');

request.mockImplementation((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'})
   })
})

   对于这种简单的mock, jest.mock() 还提供了第二种写法,它的第二个参数是一个函数,返回一个mock 实现

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'})
   })
})

  还有一种mock实现的方式,jest.spyOn(), 它接受两个参数,一个是对象,一个是对象上的某一个方法,返回一个mock函数,使用jest.spyOn() mock add方法,

 const math = require('./func')
 const addMock = jest.spyOn(math, "add"); // mock math 对象上的add方法
 addMock.mockImplementation(() => "mock"); // 提供一个实现

  使用spyOn 进行mock的好处是在同一个test 下,它可以restore, 恢复到以前默认mock的状态。这样就不用写beforeEach 和aftereEach 函数了。

const math = require('./func');
test("calls math.add", () => {   const addMock = jest.spyOn(math, "add");   // override the implementation   addMock.mockImplementation(() => "mock");   expect(addMock(12)).toEqual("mock");   // restore the original implementation   addMock.mockRestore();   expect(addMock(12)).toBeUndefined(); });

  在单元测试中,对一个函数进行mock,测试完成后函数恢复以前的样子,这也称为stub,stub就是对原函数进行替换,还有一个库sinon,就是专门做这个的。Sinon.stub(object,method) 就是对对象上的某个方法进行替换,Sinon.stub(jwt, 'verify'); jwt.verify.returns({userId: '123'}); 然后进行测试;最后jwt.verify.restore() 恢复。

  当然,spyOn 还有另外一个功能,就是监听函数,有时我们并不想mock 函数,改变函数的实现,只想监听一下它有没有被调用。

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

test('should call add', () => {
    function callMath(a, b) {
        return math.add(a + b);
    }
    const addMock = jest.spyOn(math, 'add');
    callMath(1, 2);
    expect(addMock).toBeCalled();  // toBeCalled, 就是函数有没有被调用。
})

  钩子函数 

  既然上面提了一个beforeAll, beforeEach, 就简单提一下Jest 的钩子函数,它的作用相对简单一点,就是做测试前的准备工作或测试后的清理工作。看名字也能知道它们的作用,beforeAll, 在所有测试之前做什么,beforeEach 在每一个测试之前做什么。确实会有这样的需求,比如每次测试这前都要把值恢复到初始状态。我做过这样的一个测试

if(window.unicode || window.local || window.isEnable) {
    window.history.pushState('', '', '/uat=true')
}

  要做三种情况的测试,所以每一个测试之前beforeEach, 我都把值设为了false

beforeEach(() => {
    window.unicode = false;
    window.local = false;
    window.isEnable = false
})

  要注意的是,这里的beforeEach, beforeAll 等,都是根据describe 来的,describe 表示一组测试,如果没有describe,那整个test文件就是一个describe.

describe('method called', () => {
    beforeEach(() => {
        window.unicode = false;
        window.local = false;
        window.isEnable = false
    })

    describe('another beforeEach', () => {
        beforeEach(() => {
            window.unicode = false;
            window.local = false;
            window.isEnable = false
        })
    })
})

  但有时,在做初始化的时候,并没有使用beforeEach, 但也没有什么问题,确实如此,但当describe 嵌套太多的时候,有可能就会出问题,使用console.log 输出一下,看一下执行顺序,就知道了。

describe('method called', () => {
    console.log("before each outer outer")
    beforeEach(() => {
        console.log('before each outer inner')
        window.unicode = false;
        window.local = false;
        window.isEnable = false
    })

    describe('another beforeEach', () => {
        console.log('before each inner outer');
        beforeEach(() => {
            console.log('beforeEach inner inner');
            window.unicode = false;
            window.local = false;
            window.isEnable = false
        })
    })
})

  先输出before each outer outer, 再输出了before each inner outer, 可以看到,它把describe 下面所有的没有在钩子函数里面的语句先执行了,然后再执行钩子函数,而不是按照书写的顺序进行执行,一定要注意,最好还是把所有的初始化工作放到钩子函数中.

  Jest 在进行单元测试的时候,还可以生成测试代码覆盖率的报告,只要在run jest 的时候,提供一个参数coverage。按q 退出watch模式,输入npx jest --coverage, 可以看到

  同时在根目录下,生成了一个测试coverage 目录,在lcov-report下, 有一个index.html 文件,打开,可以看到有测试了哪些文件或目录,点击目录,可以看到具体的文件,打开测试文件以后,在每一行前面会标有1x, 6x 等等,这表示这行代码执行了多小次。在代码内容上,它还有 一些标识, 比如 黑色的方块I,E, 还有黄色的标识,这都表示这个branch 没有测试,标红的代码则是直接没有测试到,需要我们去覆盖。

  Jest的基本内容说的差不多,最后再说一个babel 配置。由于Jest 默认是commonJs 规范,而我们平时用的最多的确是ES module, import 和export。 这就需要在进行单元测试之前进行转化,ES6 语法的转化,肯定是使用babel。安装babel, npm i @babel/core @babel/preset-env --save-dev  并在根目录下配置babel.config.js, babel-jest 不用安装了,安装jest的时候,已经自动安装了。

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

  这时Jest 在进行单元测试的时候,就会自动转化node 不认识的语法。

posted @ 2019-09-07 16:12  SamWeb  阅读(21039)  评论(0编辑  收藏  举报