jest對象&異步&定時器

以下是本文章內(nèi)容:

  • jest對象
    • jest.requireActual()和jest.mock()
    • jest.mocked(source,{shallow:true})
    • doMock(moduleName,factiory,options)
    • jest.spyOn()
    • jest.isMockFunction(fn)
    • jest.isMockFunction(fn)
  • 模擬異步
    • async/await
    • assertions/hasAssertions
  • 定時器
    • 啟用假計時

jest對象

函數(shù)講完了,‘生命周期’也說了,現(xiàn)在在聊聊jest這個對象。官網(wǎng)并沒有單獨對jest對象做講解,都是用到再說,因此,我總結(jié)了一下我日常生活中用到的,大家可以當(dāng)作積累來對待,甚至可以跳過這一節(jié)內(nèi)容。


jest指南:https://www.jestjs.cn/docs/setup-teardown

jest對象自動位于每個測試文件的范圍內(nèi)。對象中的方法jest有助于創(chuàng)建模擬并讓您控制 Jest 的整體行為。它也可以通過 via 顯式導(dǎo)入import {jest} from '@jest/globals'。

jest.requireActual()和jest.mock()

jest.requireActural()返回實際模塊而不是模擬,繞過所有關(guān)于模塊是否應(yīng)該接收模擬實現(xiàn)的檢查。常用于獲取模塊本身,與jest.mock一起使用

而jest.mock()模擬具有自動模擬版本的模塊,對于下面的test文件,如果jest.mock()不傳第二參數(shù)(箭頭函數(shù)),那么對應(yīng)路徑下的所有文件將被處于模擬的mock狀態(tài),即他們函數(shù)的本身行為將被抹除,因此當(dāng)我們這樣進行mock的時候jest.mock('../utils/mock/jestFn'),jestFn里面的所有函數(shù)行為變成一個假的mock,即執(zhí)行函數(shù)返回undefined,但是卻沒有mock的那些值(如calls,result等。

但是,當(dāng)我們將jest.requireActual()所返回的源模塊充當(dāng)jest.mock()的第二個回調(diào)參數(shù)的返回值時,此時jest.mock()所mock的各個函數(shù)又變回了初始磨樣,即執(zhí)行jestFn里面的方法返回值不為undefined

溫馨提示:我們的項目都是基于第一節(jié)搭建的,如果說從第一節(jié)開始,順著來的

我們新建文件夾utils/mock/jestFn.ts

export default {
    authorize: () => {
      return 'token';
    },
    isAuthorized: (secret: string) => secret === 'wizard',
};

新建測試_test/testJest.test.tsx

jest.mock('../utils/mock/jestFn',() => {
    const originModule = jest.requireActual<typeof import('../utils/mock/jestFn')>('../utils/mock/jestFn');
    return {
        __esModule: true,
        ...originModule
    }
});
import utils from '../utils/mock/jestFn';
describe('test jest something',() => {
    it('test jest object',() => {
        const res = utils.authorize();
        console.log('ll',res);
    })
})

再繼續(xù)講一點:

jestFn.ts函數(shù)是直接默認(rèn)導(dǎo)出,因此我們模擬mock重新函數(shù)的時候,函數(shù)應(yīng)該放在default中,如果jestFn我們再新加一個foo導(dǎo)出,那么模擬的時候便可以直接寫

jestFn.ts

export const foo = () => 'foo';

testJest.test.tsx

jest.mock('../utils/mock/jestFn',() => {
    ...
    return {
        ...
        default: {
            authorize: jest.fn(() => 'jack'),
            isAuthorized: jest.fn((secret: string) => secret === 'mike')
        },
        foo: () => 'mock foo'
    }
});

有人可能會問,我為啥要這么做,直接導(dǎo)入不行么?

兩個原因:其一是為了模擬函數(shù)的返回值,或部分實現(xiàn)。例如我們模擬axios,jest不會真的去發(fā)送網(wǎng)絡(luò)請求,那么我們?nèi)绻玫絘xios返回的數(shù)據(jù)并填充到頁面當(dāng)中呢,此時模擬函數(shù)的返回值就很有必要了,這樣我們在頁面渲染的時候,只需要判斷這個函數(shù)執(zhí)行即可。其二是為了測試覆蓋率,雖然我們直接將函數(shù)導(dǎo)入進來模擬也行,但是這樣就無法滿足第一點,那么,而只是將函數(shù)導(dǎo)入進來,又不使用,那么是不會覆蓋到這個jestFn文件的,因此jest.mock()也能提高覆蓋率。

jest.mocked(source,{shallow:true})

jest.mocked()模擬函數(shù)的類型定義和包裝對象的類型及其深層嵌套成員,如果不需要深層次定義,那么shallow:false。翻譯成人話就是給讓一個函數(shù)處于mock狀態(tài),這樣我們就能去設(shè)置他的mockReturnValue了,并且無視他的嵌套。

新建文件utils/mock/jestFn.ts

export const foo = (val: string) => val + 'foo';
export const song = {
    one: {
        more: {
          time: (t: number) => {
            return t;
          },
        },
    },
}

export default {
    authorize: () => {
      return 'token';
    },
    isAuthorized: (secret: string) => secret === 'wizard',
};

新建文件_test/testJestMock.test.tsx

import { song } from "../utils/mock/jestFn";

// 注意點一
jest.mock('../utils/mock/jestFn');
// 移除console
jest.spyOn(console, 'log').mockReturnValue();
// 注意點二
const mockedSong = jest.mocked<any>(song);

describe('test jest object', () => {
    it('tes jest mocked and Mocked',() => {
        console.log('---')
        mockedSong.one.more.time.mockReturnValue(12);

        expect(mockedSong.one.more.time(10)).toBe(12);
        expect(mockedSong.one.more.time.mock.calls).toHaveLength(1);
    })
})

這里有幾個注意點:首先我們在mocked函數(shù)的時候,得先把我們需要把對應(yīng)的函數(shù)路徑mock一次;其次由于是ts文件,mocked的時候必須要帶上一個any屬性,否則會提示我們找不到mockReturnValue屬性。

那既然是ts,有沒有ts專門用的呢,也有,那就是jest.MockedClass或jest.MockedFunction。jest.MockedObject,這里我是用的是第二個

新建utils/mock/type.ts

export interface LoadDD {
    username: string,
    age: number
}

在utils/mock/jestFn.ts新增方法

import type { LoadDD } from "./type";
export const loadData = (): LoadDD => {
  return {
    username: 'jack',
    age: 23
  }
}

在testJestMock.test.tsx中新增測試

import { song,loadData } from "../utils/mock/jestFn";

jest.mock('../utils/mock/jestFn');
const mock_loadData = loadData as jest.MockedFunction<typeof loadData>

it('test jest MockFunction',() => {

        mock_loadData.mockReturnValue({
            username: 'jack',
            age: 23
        })
        const res = mock_loadData();
        console.log(res,'load data')
    })

使用這個方法,我們似乎可以更好的使用類型定義,并且避免了any大法。也許你們會說,不還有一個Mocked么,確實,但是我暫時沒用到,而且由于我需要使用類型定義,那么這個方法反而不容易用到了,如需了解,可以訪問官方:
https://www.jestjs.cn/docs/mock-function-api。

doMock(moduleName,factiory,options)

當(dāng)你想在同一個文件中以不同的方式模擬一個模塊時,他就很有用,即多次mock,但是多次mock需要注意:我們需要在每次mock測試完之后,重置模塊注冊表 。我這里只介紹es6的import導(dǎo)入,如果是require可以參考官網(wǎng)(前端多用import):
https://www.jestjs.cn/docs/jest-object#jestunmockmodulename

我們新建測試_test/testDo.test.tsx

import { loadData } from "../utils/mock/jestFn";

// 移除console
jest.spyOn(console, 'log').mockReturnValue();

describe('test jest object', () => {
    beforeEach(() => {
        // 必須重置模塊,否則無法再次應(yīng)用 doMock 的內(nèi)容
        jest.resetModules();
    });
    it('test do mock 1',() => {
        jest.doMock('../utils/mock/jestFn', () => {
            return {
              __esModule: true,
              default: 'default1',
              loadData: () => {
                return {
                    username: 'jack123',
                    age: 23
                }
              },
            };
        });
        return import('../utils/mock/jestFn').then(moduleName => {
            expect(moduleName.default).toBe('default1');
            expect(moduleName.loadData()).toEqual({
                username: 'jack123',
                age: 23
            });
        });
    })

    it('test do mock 2',() => {
        jest.doMock('../utils/mock/jestFn', () => {
            return {
              __esModule: true,
              default: 'default1',
              loadData: () => {
                return {
                    username: 'jack234',
                    age: 23
                }
              },
            };
        });
        return import('../utils/mock/jestFn').then(moduleName => {
            expect(moduleName.default).toBe('default1');
            expect(moduleName.loadData()).toEqual({
                username: 'jack234',
                age: 23
            });
        });
    })
})

直接跑就完事,寫則是參照來寫,當(dāng)然,這基本上也是官網(wǎng)的寫法,我只是加上了理解與示例。

jest.spyOn()

創(chuàng)建一個類似于jest.fn但也跟蹤調(diào)用的模擬函數(shù)object[methodName]。返回 Jest模擬函數(shù),可以模擬一個文件中的某個函數(shù),類似部分模擬,同doMock類似,如果想實現(xiàn)多次模擬mock,那么我們也可以使用spyOn,并且官方也推薦我們使用,畢竟簡潔多了.

我們新建一個測試_test/testSpy.test.tsx

import * as JestFn from "../utils/mock/jestFn"

describe('test jest object2', () => {
    it('test jest spyOn',() => {
        // loadData為JestFn的一個屬性
        jest.spyOn(JestFn,'loadData').mockReturnValue({
            username: 'jack123',
            age: 23
        })
        expect(JestFn.loadData()).toEqual({
            username: 'jack123',
            age: 23
        })
    })
    it('test jest spyOn2',() => {
        // loadData為JestFn的一個屬性
        jest.spyOn(JestFn,'loadData').mockReturnValue({
            username: 'jack1234',
            age: 23
        })
        expect(JestFn.loadData()).toEqual({
            username: 'jack1234',
            age: 23
        })
    })
})

如果需要模擬函數(shù)重新實現(xiàn),則使用mockImplementation,并且我們也結(jié)合了前面的jestMockedFunction

我們給testSpy.test.tsx加一個測試

import * as JestFn from "../utils/mock/jestFn";

const mock_foo = jest.spyOn(JestFn,'foo') as jest.MockedFunction<typeof JestFn.foo>
mock_foo.mockImplementation(
    (val: string) => val + '_foo'
)

describe('test jest object2', () => {
    it('test jest spyOn implementation',() => {
        const res = mock_foo('zl');
        console.log(res,'mock implementation')
    })
   ...
})

jest.isMockFunction(fn)

確定給定函數(shù)是否為模擬函數(shù),感覺沒啥用,根據(jù)我的測試,只有使用的函數(shù)是jest.fn()才返回true。加上它是因為我看到別人用過了。。。

jest.spied

也用處不大,并且官方也建議,如果需要模擬函數(shù)的重新實現(xiàn),可以使用mockImplementation,理由也是同上。

jest對象中還包含了計時器,但是在說計時器之前,我們先聊一聊異步的模擬,畢竟計時器就可以說是異步了。

模擬異步

異步運行代碼在 JavaScript 中很常見。當(dāng)您有異步運行的代碼時,Jest 需要知道它正在測試的代碼何時完成,然后才能繼續(xù)進行另一個測試。Jest 有幾種方法來處理這個問題,都是一個個真實的實例,需要大家親自驗證!

模擬async/await

我們新建文件utils/async/index.ts

export const getDD = (count: number) => {
    return new Promise((resolve,reject) => {
        if (count > 20) {
            reject('count is too large')
        } else {
            resolve('count is right')
        }
    })
}

然后新建測試文件_test/jest/testAsync.test/tsx

import { getDD } from "utils/async";

describe('test async',() => {
    it('test await/async', async () => {
        const res2 = await getDD(10);
        expect(res2).toEqual('count is right');
        try {
            await getDD(10);
        } catch (e) {
            expect(e).toBe('count is too large');
        }
    })
})

注:這里我把jest對象相關(guān)的測試放置到一個文件夾了


這個是我此時測試文件的層級展示

這樣我們就模擬了一個異步了。

我們也可以使用異步的語法糖resolves/rejects去判斷,語法糖可以用一個await或者return,如下:

describe('test async',() => {
    ...
    it('test syntastic sugar',async () => {
        await expect(getDD(10)).resolves.toBe('count is right');
        // return expect(getDD(10)).resolves.toBe('count is right');
        await expect(getDD(30)).rejects.toBe('count is too large');
        // return expect(getDD(30)).rejects.toBe('count is too large');
    })
})

assertions/hasAssertions

驗證在測試期間調(diào)用了一定數(shù)量的斷言。這在測試異步代碼時通常很有用,以確?;卣{(diào)中的斷言確實被調(diào)用。說人話就是提前預(yù)定有幾個異步,比如我上面只有一個異步,那么就可以這樣寫;而hasAssertions就是判斷是否有異步代碼,額不就等同于assertions(>1)。我認(rèn)為這個有用,在于必須提前知道異步的數(shù)量,那么如何知道呢?一個是項目是你自己寫的,自然可以數(shù)出來有多少個,另一個是直接寫一個很大的值assertions(1000),這樣肯定報錯,然后報錯就會告訴你有幾個異步了。。。

it('test await/async', async () => {
    expect.hasAssertions();
    expect.assertions(1);
    const res2 = await getDD(10);
    expect(res2).toEqual('count is right');
    try {
      await getDD(10);
    } catch (e) {
      expect(e).toBe('count is too large');
    }
})

定時器

當(dāng)我們了解了異步的這個概念之后,現(xiàn)在我們可以開始說定時器。定時器模擬是jest對象介紹的擴展,因為接口也是jest的,但是我這里單獨拿出來做講解,并且在有些時候,定時器模擬會給你帶來覆蓋率的提升。

啟用假計時

新建文件utils/fake/timeGame.ts

type CallBack = () => void

export const timerGame = (callback: CallBack) => {
    console.log('Ready....go!');
    setTimeout(() => {
      console.log("Time's up -- stop!");
      callback && callback();
    }, 1000);
}

新建測試_test/fake/timeGame.test.tsx

import { timerGame } from "utils/fake/timeGame";

jest.useFakeTimers();
jest.spyOn(global, 'setTimeout');

describe('test fake time',() => {
    it('test start fake',() => {
        const fn = jest.fn()
        timerGame(fn);
        expect(setTimeout).toHaveBeenCalledTimes(1);
        expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function),1000);
        // 打?。篟eady....go!
    })
})

測試我們只會打?。?* Ready....go!**

然后我們運行計時器:jest.runAllTimes(),將執(zhí)行所有掛起的宏任務(wù)和微任務(wù)

測試代碼調(diào)整:

...
describe('test fake time',() => {
    it('test start fake',() => {
        ...
        expect(fn).not.toBeCalled();
        jest.runAllTimers();
        expect(fn).toBeCalled();
        expect(fn).toBeCalledTimes(1);
        // 此時先后打?。?        // Ready....go!
        // Time's up -- stop!
    })
})

以上便是定時器的使用,附帶兩個官網(wǎng)地址:

定時器:
https://www.jestjs.cn/docs/timer-mocks

api: https://www.jestjs.cn/docs/jest-object#jestadvancetimerstonexttimersteps

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 通過前面的介紹,我們基本上對jest有了一個初步的了解,并搭建了環(huán)境開始上手測試,接下來就開始說函數(shù)。本節(jié)內(nèi)容主要...
    溪風(fēng)_耶羅閱讀 2,522評論 1 1
  • 翻譯自:https://jestjs.io/docs/zh-Hans/configuration最新更新:2020...
    秋名山車神12138閱讀 21,989評論 0 7
  • 這篇文章你可以學(xué)到以下知識: jest入門開始從sum開始的test配置別名(important)測試對象 jes...
    溪風(fēng)_耶羅閱讀 264評論 0 1
  • 前言 單元測試是一種用于測試“單元”的軟件測試方法,其中“單元”的意思是指軟件中各個獨立的組件或模塊。開發(fā)者需要為...
    袋鼠云數(shù)棧前端閱讀 435評論 0 1
  • 對于一個完整的前端工程,單元測試是不可缺少的一部分。但我們之所以很少使用單元測試,是對單元測試的認(rèn)知不夠,所以接下...
    Fiorile閱讀 1,231評論 0 1

友情鏈接更多精彩內(nèi)容