什么是單元測(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ù)交流哦。