大多數(shù)開發(fā)人員不知道如何測試
每個開發(fā)人員都知道我們應該編寫單元測試,以防止將缺陷部署到生產中。
大多數(shù)開發(fā)人員不知道的是每個單元測試的基本要素。我無法開始計算我看到單元測試失敗的次數(shù),完全不知道開發(fā)人員試圖測試什么功能,更不用說它出了什么問題或為什么重要。
在我最近的一個項目中,我們讓一大片單元測試進入測試套件,但完全沒有描述測試的目的。我們有一個很棒的團隊,所以我放松了。結果?只有作者才能真正理解的大量單元測試。
幸運的是,我們完全重新設計了API,我們將把整個套件扔掉并從頭開始 - 這將是我修復列表中的優(yōu)先級#1。
不要讓這件事發(fā)生在你身上。
為什么進行測試?
您的測試是防范軟件缺陷的第一道防線。您的測試比linting和靜態(tài)分析更重要(它只能找到錯誤的子類,而不是實際程序邏輯的問題)。測試與實現(xiàn)本身一樣重要(重要的是代碼滿足要求 - 如果實現(xiàn)得不好,它的實現(xiàn)方式根本不重要)。
單元測試結合了許多功能,使它們成為應用程序成功的秘密武器:
- 設計輔助:首先編寫測試可以讓您更清晰地了解理想的API設計。
- 功能文檔(面向開發(fā)人員):測試描述在代碼中包含每個實現(xiàn)的功能要求。
- 測試開發(fā)人員的理解:開發(fā)人員是否足夠理解問題,以便在代碼中闡明所有關鍵組件的要求?
- 質量保證:手動QA容易出錯。根據(jù)我的經驗,開發(fā)人員在更改重構,添加新功能或刪除功能后,無法記住需要測試的所有功能。
- 持續(xù)交付援助:自動化質量保證提供了自動防止破壞的構建部署到生產的機會。
單元測試不需要扭曲或操縱來滿足所有這些廣泛的目標。相反,單元測試的基本性質是滿足所有這些需求。這些好處都是良好編寫的具有良好覆蓋率的測試套件的副作用。
TDD(測試驅動開發(fā))
有證據(jù)說:
- TDD可以降低錯誤密度。
- TDD可以鼓勵更多模塊化設計(提高軟件靈活性/團隊速度)。
- TDD可以降低代碼復雜性。
科學說:大量的經驗證據(jù)表明TDD有效**。
先寫測試
Microsoft Research,IBM和Springer tested 優(yōu)先測試與后測試方法的效果,并始終發(fā)現(xiàn)優(yōu)先測試處理比后來添加測試產生更好的結果。它非常明確:在您實施之前,請編寫測試。
在實施之前,寫測試。
什么是良好的單元測試?
好的,所以TDD有效。首先編寫測試。要更有紀律。相信這個過程......我們明白了。但是你怎么寫一個好的單元測試?
我們將從一個真實的項目中看一個非常簡單的例子來探索這個過程:來自Stamp Specification的compose()函數(shù)。
我們將use tape進行測試,因為它有著透明度和簡潔性。
在我們回答如何編寫良好的單元測試之前,首先我們必須了解如何使用單元測試:
- 設計輔助:在設計階段,在實施之前編寫。
- 功能文檔和開發(fā)人員理解測試:測試應提供正在測試的功能的清晰描述。
- 質量保證/持續(xù)交付:測試應在故障時停止交付管道,并在失敗時生成錯誤報告。
單元測試作為Bug報告
當測試失敗時,該測試失敗報告通常是您關于確切出錯的第一個也是最好的線索 - 快速追蹤根本原因的秘訣就是知道從哪里開始尋找。當您有一個非常明確的錯誤報告時,這個過程會變得更加容易。
失敗的測試應該讀起來像高質量的bug報告。
好的測試失敗bug報告中包含什么?
- 你在測試什么?
- 它應該做什么?
- 輸出是什么(實際)?
- 預期輸出(預期行為)是什么??

良好故障報告的示例
首先回答“你在測試什么?”:
- 您正在測試哪些組件方面?
- 該功能應該做什么?您正在測試哪些特定的行為要求?
The compose() function takes any number of stamps (composable factory functions) and produces a new stamp.
compose()函數(shù)應該測試的內容,并產生相應的結果。
要編寫此測試,我們將從任何單個測試的最終目標向后工作:測試特定的行為要求。為了使這個測試通過,代碼必須產生什么特定的行為?
該功能應該做什么?
我喜歡從寫一個字符串開始。沒有分配給任何東西。沒有傳遞給任何函數(shù)。只需明確關注組件必須滿足的特定要求。在這種情況下,我們將從compose()函數(shù)返回一個函數(shù)開始。
一個簡單,可測試的要求:
'compose()應該返回一個函數(shù)。'
現(xiàn)在我們將跳過一些內容并充實其余的測試。這個字符串是我們的目標。事先說明它有助于我們關注獎品。
我們測試的組件方面是什么?
“組件方面”的含義因測試而異,具體取決于為測試組件提供足夠覆蓋所需的粒度。
在這種情況下,我們將測試compose()函數(shù)的返回類型,以確保它返回正確類型的東西,而不是undefined或什么都沒有,因為它在你運行它時拋出。
讓我們把這個問題轉換成測試代碼。答案出現(xiàn)在測試描述中。這一步也是我們調用函數(shù)并傳遞回調函數(shù)的地方,當測試運行時,測試運行器將調用這個回調函數(shù):
test('<What component aspect are we testing?>', assert => {
});
在這種情況下,我們正在測試compose函數(shù)的輸出:
test('Compose function output type.', assert => {
});
當然,我們仍然需要我們的第一個描述。它進入回調函數(shù):
test('Compose function output type.', assert => {
'compose() should return a function.'
});
什么是輸出(預期和實際)?
equal()是我最喜歡的斷言。如果每個測試套件中唯一可用的斷言是“equal()”,那么世界上幾乎每個測試套件都會更好。為什么?
因為equal(),本質上回答了每個單元測試必須回答的兩個最重要的問題,但大多數(shù)不會:
- 什么是實際的輸出?
- 什么是預期的輸出?
如果您在沒有回答這兩個問題的情況下完成測試,那么您就沒有真正的單元測試。你有一個草率,半生不熟的測試。
如果您只從本文中獲取一件事,那么就這樣:
Equal是你的新默認斷言。
它是每個優(yōu)秀測試套件的主要內容。
所有那些帶有數(shù)百種不同花哨斷言的花哨的斷言庫都會破壞測試的質量。
一個挑戰(zhàn)
想要更好地編寫單元測試?對于下周,嘗試使用equal()或deepEqual()編寫每個斷言,或者在您選擇的斷言庫中使用它們的等價物。不要擔心對您的套件的質量影響。我的錢說這項練習將大大改善它。
這在代碼中是什么樣的?
const actual = '<what is the actual output?>';
const expected = '<what is the expected output?>';
第一個問題確實在測試失敗中起到雙重作用。通過回答這個問題,您的代碼也會回答另一個問題:
const actual = '<how is the test reproduced?>';
需要注意的是很重要的actual*值必須通過行使某些組件的公共API的生產。否則,測試沒有價值。我已經看到測試套件如此淹沒了模擬和存根以及鈴聲和口哨,一些測試從未運用任何據(jù)稱正在測試的代碼。
讓我們回到這個例子:
const actual = typeof compose();
const expected = 'function';
你可以構建一個斷言,而不是專門為名為actual和expected的變量賦值,但我最近開始專門為每個測試中的actual和expected 變量賦值,發(fā)現(xiàn)它使我的測試更容易讀。
看看它如何澄清斷言?
assert.equal(actual, expected,
'compose() should return a function.');
它將“如何”與測試體中的“什么”分開。
- 想知道我們如何得到結果?查看變量賦值。
- 想知道我們正在測試什么?看一下斷言的描述。
結果是測試本身的讀取就像高質量的錯誤報告一樣容易。
讓我們看一下上下文中的所有內容:
import test from 'tape';
import compose from '../source/compose';
test('Compose function output type', assert => {
const actual = typeof compose();
const expected = 'function';
assert.equal(actual, expected,
'compose() should return a function.');
assert.end();
});
下次編寫測試時,請記住回答所有問題:
- 你在測試什么?
- 它該怎么辦?
- 什么是實際的輸出?
- 什么是預期的輸出?
- 如何復制測試?
最后一個問題由用于派生“actual”值的代碼來回答。
單元測試模板:
import test from 'tape';
// For each unit test you write,
// answer these questions:
test('What component aspect are you testing?', assert => {
const actual = 'What is the actual output?';
const expected = 'What is the expected output?';
assert.equal(actual, expected,
'What should the feature do?');
assert.end();
});
使用單元測試還有很多,但知道如何編寫一個好的測試還有很長的路要走。