Jest 單元測試入門

  首先是爲何要寫單元測試? 主要仍是測試咱們代碼有沒有達到預期的效果,其次,若是嚴格按照TDD(測試驅動開發)來進行開發的話,咱們還會更加註重產品細節,代碼可能更加健壯。由於TDD是測試放到第一位,寫代碼以前,先寫測試。測試怎麼寫?確定是思考產品的各類使用場景,以及在每種場景下,會有什麼效果或異常,思考好了,測試寫完了,整個產品也就是一清二楚了。真正寫代碼的時候,只是把測試場景用代碼實現,這些所謂的場景就是一個個測試用例。html

  其次是怎麼寫單元測試。測試的目的就是看看代碼有沒有達到咱們的預期,那確定是先引入要測試的代碼,而後是運行代碼,最後再和咱們的預期作比較,若是和預期一致,就代表代碼沒有問題,若是沒有達到預期, 就是代碼有問題。和預期進行比較,就是斷言。這也就是意味着,每個測試中最終都會落腳到一個斷言。若是沒有斷言(比較),測試也沒有意義,代碼運行完了,結果呢?不知道。斷言就是比較,判斷正不正確,對不對,1 + 1 是否是等於2, 就是一個最簡單的斷言,1+1是計算,程序的運行,2就是指望或預期,等於就是判斷或比較,具體到測試代碼(jest)中,就是expect(1+1).toBe(2). 在測試程序中, 只要看到expect 什麼, to 什麼,這就是斷言,還有多是assert.node

  最後是運行測試代碼。單元測試寫了,確定要運行,不運行,怎麼知道結果。這裏是我學習時最大的困惑,想明白了,有些問題也就迎刃而解了。當咱們運行單元測試,就是在命令行中輸入jest的時候,其實是在node 環境下執行js代碼。使用jest 進行單元測試,就是啓動了一個node程序來執行測試代碼。每當遇到測試問題的時候,先想想這個前提,說不定,問題就解決了。git

  好了,說的也差很少了,那就一塊兒來學習一下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 moduleajax

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

   函數寫完了,那怎麼測試呢,測試代碼放到什麼地方呢?Jest識別三種測試文件,以.test.js結尾的文件,以.spec.js結尾的文件,和放到__tests__ 文件夾中的文件。Jest 在進行測試的時候,它會在整個項目進行查找,只要碰到這三種文件它都會執行。乾脆,再寫兩個函數,用三種測試文件分別進行測試, func.js 以下npm

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 以下json

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 測試。promise

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,bash

  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(); });

  固然,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 不認識的語法。

相關文章
相關標籤/搜索