單元測(cè)試與單元測(cè)試框架 Jest

什么是單元測(cè)試?

測(cè)試是一種驗(yàn)證我們的代碼是否可以按預(yù)期工作的手段。

被測(cè)試的對(duì)象可以是我們程序的任何一個(gè)組成部分。大到一個(gè)分為多步驟的下單流程袍辞,小到代碼中的一個(gè)函數(shù)。

單元測(cè)試特指被測(cè)試對(duì)象為程序中最小組成單元的測(cè)試常摧。這里的最小組成單元可以是一個(gè)函數(shù)搅吁、一個(gè)類等等。

單元測(cè)試的優(yōu)勢(shì)

由于被測(cè)試對(duì)象的簡(jiǎn)單(通常只有一個(gè)或多個(gè)輸入以及一個(gè)輸出)落午,這就決定了單元測(cè)試開(kāi)發(fā)起來(lái)也很簡(jiǎn)單谎懦,通常每個(gè)測(cè)試只有幾行到十幾行不等。測(cè)試代碼的簡(jiǎn)單表示它可以被更頻繁的執(zhí)行(事實(shí)上溃斋,很多單元測(cè)試框架都有 watch 模式界拦。每次改動(dòng)代碼時(shí)都會(huì)自動(dòng)執(zhí)行單元測(cè)試)。更頻繁的執(zhí)行意味著更早的發(fā)現(xiàn)問(wèn)題梗劫。

試想享甸,隨著代碼的不斷迭代,程序中總有某些位置會(huì)頻繁出現(xiàn)某類問(wèn)題梳侨。在沒(méi)有單元測(cè)試時(shí)程序員之間往往都是“口口相傳”枪萄,隔一段時(shí)間很可能由于疏忽還會(huì)犯同一個(gè)錯(cuò)誤。有了單元測(cè)試我們就可以為這些問(wèn)題點(diǎn)編寫(xiě)對(duì)應(yīng)的測(cè)試代碼猫妙,每次提交代碼前都執(zhí)行一遍,可以極大的降低相同 bug 重復(fù)出現(xiàn)的概率聚凹。

此外割坠,要為一個(gè)被測(cè)試對(duì)象編寫(xiě)單元測(cè)試,那么它應(yīng)該首先是容易被測(cè)試的(這似乎是一句廢話)妒牙。反過(guò)來(lái)講彼哼,如果你面對(duì)一個(gè)函數(shù)歇终、類卻很難編寫(xiě)測(cè)試代碼的時(shí)候鳖轰,很可能是你的代碼設(shè)計(jì)上存在問(wèn)題介评。比如和外部依賴耦合過(guò)于緊密痘括。這種情況下萍倡,編寫(xiě)單元測(cè)試的過(guò)程會(huì)倒逼我們優(yōu)化我們代碼的結(jié)構(gòu)乱灵。將復(fù)雜的代碼拆解成為更簡(jiǎn)單孵延、更容易測(cè)試的片段苟跪。這個(gè)過(guò)程本身也會(huì)潛移默化的提高我們代碼的質(zhì)量旗们。

單元測(cè)試的限制/不足

I get paid for code that works, not for tests - Kent Beck

首先蚓哩,測(cè)試代碼再簡(jiǎn)單,也是需要工作量來(lái)開(kāi)發(fā)的上渴。必定占用開(kāi)發(fā)人員的時(shí)間岸梨。因此需要開(kāi)發(fā)人員在投入與收益之間找到一個(gè)最佳的平衡點(diǎn)喜颁。

其次,單元測(cè)試覆蓋率往往會(huì)給開(kāi)發(fā)人員一種錯(cuò)覺(jué):這段代碼的單元測(cè)試都通過(guò)了(測(cè)試覆蓋率以及 100% 了)曹阔,肯定沒(méi)有 bug半开。其實(shí)不然,單元測(cè)試覆蓋率與代碼質(zhì)量沒(méi)有必然的聯(lián)系赃份。作為開(kāi)發(fā)人員必須盡早認(rèn)識(shí)到這一點(diǎn)寂拆。

何時(shí)編寫(xiě)單元測(cè)試?

  • 開(kāi)發(fā)過(guò)程中芥炭,單元測(cè)試應(yīng)該來(lái)測(cè)試那些可能會(huì)出錯(cuò)的地方漓库,或是那些邊界情況。
  • 維護(hù)過(guò)程中园蝠,單元測(cè)試應(yīng)該圍繞著 bug 進(jìn)行渺蒿,每個(gè) bug 都應(yīng)該編寫(xiě)響應(yīng)的單元測(cè)試。從而保證同一個(gè) bug 不會(huì)出現(xiàn)第二次彪薛。

單元測(cè)試中的基本概念茂装?

單元測(cè)試一般包含以下幾個(gè)部分:

  • 被測(cè)試的對(duì)象是什么
  • 要測(cè)試該對(duì)象的什么功能
  • 實(shí)際得到的結(jié)果
  • 期望的結(jié)果
  • mock / spy (下文會(huì)詳述)

具體到某個(gè)單元測(cè)試,往往包含以下幾個(gè)步驟:

  • 準(zhǔn)備階段:構(gòu)造參數(shù)善延,創(chuàng)建 spy 等
  • 執(zhí)行階段:用構(gòu)造好的參數(shù)執(zhí)行被測(cè)試代碼
  • 斷言階段:用實(shí)際得到的結(jié)果與期望的結(jié)果比較少态,以判斷該測(cè)試是否正常
  • 清理階段:清理準(zhǔn)備階段對(duì)外部環(huán)境的影響,移除在準(zhǔn)備階段創(chuàng)建的 spy 等

Jest 簡(jiǎn)介

Jest是 Facebook 開(kāi)發(fā)的一款 JavaScript 測(cè)試框架易遣。在 Facebook 內(nèi)部廣泛用來(lái)測(cè)試各種 JavaScript 代碼彼妻。其官網(wǎng)上主要列出了以下幾個(gè)特點(diǎn):

  • 輕松上手
    • 使用 create-react-app或是 react-native init創(chuàng)建的項(xiàng)目已經(jīng)默認(rèn)集成了 Jest
    • 現(xiàn)有項(xiàng)目,只需創(chuàng)建一個(gè)名為 __test__ 的目錄豆茫,然后在該目錄中創(chuàng)建以 .spec.js.test.js 結(jié)尾的文件即可
  • 內(nèi)置強(qiáng)大的斷言與 mock 功能
  • 內(nèi)置測(cè)試覆蓋率統(tǒng)計(jì)功能
  • 內(nèi)置 Snapshot 機(jī)制

雖然 Jest 官網(wǎng)介紹中多次 React侨歉,但實(shí)際上 Jest 并不是和 React 綁定的。你可以使用它測(cè)試任何 JavaScript 項(xiàng)目揩魂。

Jest 基礎(chǔ)功能介紹

安裝:

npm install --save-dev jest

然后配置 package.json

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

接著創(chuàng)建一個(gè)名為 __tests__ 的目錄幽邓。jest 會(huì)自動(dòng)去該目錄下尋找要執(zhí)行的測(cè)試代碼。

接下來(lái)讓我們編寫(xiě)一個(gè)最簡(jiǎn)單測(cè)試火脉。

describe('Addition', () => {
  it('knows that 2 and 2 make 4', () => {
    const val1 = 2;
    const val2 = 2;
    const result = val1 + val2;
    const expectedResult = 4;
    expect(result).toBe(expectedResult);
  });
});

接下來(lái)讓我們看看這個(gè)單元測(cè)試是否滿足了我們前文提到的元素與步驟牵舵。

元素:

  • 被測(cè)試的對(duì)象是什么: + 運(yùn)算符
  • 要測(cè)試該對(duì)象的什么功能: 2 + 2 = 4
  • 實(shí)際得到的結(jié)果:result
  • 期望的結(jié)果: expectedResult

步驟:

  • 準(zhǔn)備階段:line3, line4
  • 執(zhí)行階段:line5
  • 斷言階段:line7
  • 清理階段:無(wú)

可以看出,單元測(cè)試的編寫(xiě)是有“套路”可循的倦挂。實(shí)際中畸颅,我們一般不會(huì)創(chuàng)建這么多臨時(shí)變量,可以簡(jiǎn)寫(xiě)成:

describe('Addition', () => {
  it('knows that 2 and 2 make 4', () => {
      expect(2 + 2).toBe(4);
  });
});

toBe 只是 Jest 強(qiáng)大斷言功能中的一個(gè)方法妒峦。

現(xiàn)在讓我們來(lái)執(zhí)行一下剛剛編寫(xiě)的測(cè)試代碼吧:

Jest 中的 mock 與 spy

讓我們來(lái)通過(guò)例子了解 mock 與 spy重斑。

假設(shè)有下面這個(gè)函數(shù):

function forEach(items, callback) {
  for (let index = 0; index < items.length; index++) {
    callback(items[index]);
  }
}

功能很簡(jiǎn)單,循環(huán)第一個(gè)參數(shù) items肯骇,并把數(shù)組中的每一項(xiàng)作為參數(shù)調(diào)用第二個(gè)參數(shù) callback窥浪。該如何測(cè)試呢祖很?

我們要?jiǎng)?chuàng)建一個(gè)特殊的 callback 函數(shù),它可以記錄每次調(diào)用時(shí)傳入的參數(shù)供我們進(jìn)行斷言漾脂。

下面是一段示例代碼:

describe('forEach', () => {
  it('should call callback with each item', () => {
    const callHistory = [];
    const specialCallback = (...args) => callHistory.push(args);
    forEach([1, 2], specialCallback);

    expect(callHistory.length).toBe(2);
    expect(callHistory[0][0]).toBe(1);
    expect(callHistory[1][0]).toBe(2);
  })
});

這里的 specialCallback 就是一個(gè) mock假颇。它存在的意義就是統(tǒng)計(jì)函數(shù)被調(diào)用的信息供我們使用。這種模式在單元測(cè)試中經(jīng)常被使用骨稿,所以 Jest 已經(jīng)內(nèi)置了對(duì) mock 的支持笨鸡。讓我們來(lái)看看如何使用:

describe('forEach', () => {
  it('should call callback with each item', () => {
    const mockFn = jest.fn();
    forEach([1, 2], mockFn);

    expect(mockFn.mock.calls.length).toBe(2);
    expect(mockFn.mock.calls[0][0]).toBe(1);
    expect(mockFn.mock.calls[1][0]).toBe(2);
  })
});

很方便吧,只需要 jest.fn() 一下就可以得到一個(gè)功能搶到的 mock 函數(shù)坦冠。

最后再來(lái)說(shuō)一下 spy形耗。其實(shí) spy 和 mock 是非常類似的,唯一的區(qū)別點(diǎn)在于辙浑,spy 用于監(jiān)聽(tīng)一個(gè)現(xiàn)有對(duì)象上的方法激涤。

還是通過(guò)一個(gè)例子來(lái)看,假設(shè)我們有對(duì)象:

const bot = {
  sayHello: (name) => {
    console.log(`Hello ${name}!`);
  }
}

我們可以像下面這樣創(chuàng)建并使用 spy:

describe('bot', () => {
  it('should say hello', () => {
    const spy = jest.spyOn(bot, 'sayHello');

    bot.sayHello('Michael');

    expect(spy).toHaveBeenCalledWith('Michael');

    spy.mockRestore();
  })
});

我們通過(guò) jest.spyOn 創(chuàng)建了一個(gè)監(jiān)聽(tīng) bot 對(duì)象的 sayHello 方法的 spy判呕。它就像間諜一樣監(jiān)聽(tīng)了所有對(duì) bot#sayHello 方法的調(diào)用倦踢。由于創(chuàng)建 spy 時(shí),Jest 實(shí)際上修改了 bot 對(duì)象的 sayHello 屬性侠草,所以在斷言完成后辱挥,我們還要通過(guò) mockRestore 來(lái)恢復(fù) bot 對(duì)象原本的 sayHello 方法。

實(shí)戰(zhàn):使用 Jest 編寫(xiě)一個(gè)完整的單元測(cè)試

到這里边涕,單元測(cè)試的套路和 Jest 的基本用法已經(jīng)介紹的差不多了晤碘。讓我們最后通過(guò)一個(gè)完整的示例來(lái)結(jié)束今天的討論。

被測(cè)試的函數(shù)名為 getImageDomain功蜓。主要功能就是為某個(gè) skuId 選取一個(gè)圖片服務(wù)器域名哼蛆,如果未傳入 skuId,則隨機(jī)返回一個(gè)域名:

const domains = [
  'img10.360buyimg.com',
  'img11.360buyimg.com',
  'img12.360buyimg.com',
  'img13.360buyimg.com',
  'img14.360buyimg.com',
];

const getImageDomain = (skuId) => {
  if (skuId) {
    return domains[skuId % 5];
  } else {
    return domains[Math.floor(Math.random() * 5)];
  }
}

對(duì)應(yīng)的測(cè)試代碼如下霞赫,由于邏輯比較簡(jiǎn)單,故不再詳細(xì)分析:

describe('getImageDomain', () => {
  it('should select domain based on skuId if provided', () => {
    expect(getImageDomain(1)).toBe('img11.360buyimg.com');
  });

  it('should select a random domain based on Math.random if skuId not available', () => {
    const spy = jest.spyOn(Math, 'random').mockImplementation(() => 0.9);

    expect(getImageDomain()).toBe('img14.360buyimg.com');
    expect(spy).toHaveBeenCalled();

    spy.mockRestore();
  });
});

寫(xiě)在最后

測(cè)試只是一種手段肥矢,而不是目的端衰。

軟件的質(zhì)量不是測(cè)試出來(lái)的,而是設(shè)計(jì)和維護(hù)出來(lái)的甘改。

以上內(nèi)容就是本篇的全部?jī)?nèi)容以上內(nèi)容希望對(duì)你有幫助旅东,有被幫助到的朋友歡迎點(diǎn)贊,評(píng)論十艾。
如果對(duì)軟件測(cè)試抵代、接口測(cè)試、自動(dòng)化測(cè)試忘嫉、面試經(jīng)驗(yàn)交流荤牍。感興趣可以關(guān)注小編案腺,會(huì)有同行一起技術(shù)交流哦。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末康吵,一起剝皮案震驚了整個(gè)濱河市劈榨,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌晦嵌,老刑警劉巖同辣,帶你破解...
    沈念sama閱讀 222,627評(píng)論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異惭载,居然都是意外死亡旱函,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,180評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén)描滔,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)棒妨,“玉大人,你說(shuō)我怎么就攤上這事伴挚“醒埽” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 169,346評(píng)論 0 362
  • 文/不壞的土叔 我叫張陵茎芋,是天一觀的道長(zhǎng)颅眶。 經(jīng)常有香客問(wèn)我,道長(zhǎng)田弥,這世上最難降的妖魔是什么涛酗? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 60,097評(píng)論 1 300
  • 正文 為了忘掉前任,我火速辦了婚禮偷厦,結(jié)果婚禮上商叹,老公的妹妹穿的比我還像新娘。我一直安慰自己只泼,他們只是感情好剖笙,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,100評(píng)論 6 398
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著请唱,像睡著了一般弥咪。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上十绑,一...
    開(kāi)封第一講書(shū)人閱讀 52,696評(píng)論 1 312
  • 那天聚至,我揣著相機(jī)與錄音,去河邊找鬼本橙。 笑死扳躬,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播贷币,決...
    沈念sama閱讀 41,165評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼击胜,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了片择?” 一聲冷哼從身側(cè)響起潜的,我...
    開(kāi)封第一講書(shū)人閱讀 40,108評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎字管,沒(méi)想到半個(gè)月后啰挪,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,646評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡嘲叔,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,709評(píng)論 3 342
  • 正文 我和宋清朗相戀三年亡呵,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片硫戈。...
    茶點(diǎn)故事閱讀 40,861評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡锰什,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出丁逝,到底是詐尸還是另有隱情汁胆,我是刑警寧澤,帶...
    沈念sama閱讀 36,527評(píng)論 5 351
  • 正文 年R本政府宣布霜幼,位于F島的核電站嫩码,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏罪既。R本人自食惡果不足惜铸题,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,196評(píng)論 3 336
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望琢感。 院中可真熱鬧丢间,春花似錦、人聲如沸驹针。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,698評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)柬甥。三九已至墙牌,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間暗甥,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,804評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工捉捅, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留撤防,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 49,287評(píng)論 3 379
  • 正文 我出身青樓棒口,卻偏偏與公主長(zhǎng)得像寄月,于是被迫代替她去往敵國(guó)和親辜膝。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,860評(píng)論 2 361