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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM