單元測試到底是什麼?應該怎麼做?

大學寫了幾年的c++,一直聽說有個東西叫做單元測試,但是一直不懂到底什麼是單元測試,也不知道該怎麼做。之前測試都是寫個test.h包含到cpp中,更換.h就更換測試。後來到網上查閱資料,多數都是講了單元測試的好處就開始寫代碼,寫範例,但還是一頭霧水,做個測試為什麼要寫那麼多代碼呢?難道是我理解的單元測試不對?有沒有前輩從單元測試的興起、發展講解一下,到底什麼是單元測試,又到底該怎麼做?


代碼是為了什麼,當然是為了重複運行。如何保持unit test代碼的穩定?主要靠好的API設計。API切實正確切割了需求,那麼在重構的時候API就基本不用變化,unit test也不用重寫。以後你重構的時候,只要你的unit test覆蓋的夠好,基本跑一遍就知道有沒有改成傻逼。可以節省大量的時間。

所以那些專門寫不需要維護的軟體的人,討厭測試,也是情有可原的。


軟體工程術語 Unit testing 中的 Unit(單元)有多種含義,差不多就是程序模塊(Module)的意思。

Wikipedia: Unit testing

你的程序主要是由一個個的 Class 組成的,一個類或一個對象當然也是一個單元,而比類更小的單元是類的方法(函式)。如果你的類中的基本單元——如某些方法不能正常工作,在某些輸入條件下會得出錯誤的執行結果,那麼如何保證你的類/對象乃至整個應用軟體或系統作為一個整體能正常工作呢?所以,簡單說,單元測試(優先)的目的就是首先保證一個系統的基本組成單元、模塊(如對象以及對象中的方法)能正常工作,這是一種分而治之中的 bottom-up 思想。

2000 年前,我在國內很少聽說單元測試,或者有人在積極地做自動單元測試。主要原因還是因為當時缺乏好的工具支持,而且大家對自動單元測試的重要性、必要性與價值也缺乏足夠深入的認識。

最近這 15 年,隨著敏捷運動以及 Java 等新一代 OO 語言、技術和開發工具的興起,自動單元測試在實踐中得到了大面積普及,這主要還是得益於 Kent Beck、Erich Gamma 兩位大師聯合為 Eclipse IDE 開發的自動單元測試工具 JUnit 的一舉成名,以及後來噴涌而出的龐大 xUnit 家族(其中就包括 cppUnit)。xUnit 系列工具其實都源於 1998 年 Kent Beck 為 SmallTalk 語言開發的自動單測工具 SUnit。所以,在普及自動單元測試這件事情上,Kent Beck 作為先驅確實功不可沒!

Wikipedia:xUnit
Wikipedia:List of unit testing frameworks

還是一頭霧水,做個測試為什麼要寫那麼多代碼呢?

假設你的 APP 有 100 個類,平均每個類有 10 個方法。如何為這 1000 個方法寫單元測試?

。。。


這個問題實質就是投入產出問題,是設計問題。
單元測試大小公司大小項目都在嘗試採用,而且也不乏成功的例子,QT項目連界面都使用了單元測試。
但是初期實施非常艱難,我以前的項目堅持了一段時間後來除了我之外,沒人編寫單元測試,如果要求的話,也僅僅是後期補上單元測試。
現在除了千行以內的原型項目外,所有產品級的項目我都推薦添加單元測試。界面單元測試部分視情況添加,因為大多數桌面系統項目跟QT項目有著本質的區別,它是提供用戶一致性的窗體組件,而客戶端系統提供客戶千差萬別的操作需求,這需求變動還非常快,基本上投入高產出低。
單元測試對框架的設計要求非常高,數據與代碼與界面要儘可能分離,介面定義,輸出與輸出預期,代碼覆蓋度,基本上跟白盒測試差不多的意思。
最後不得不說的是多線程的單元測試,這塊我的意見是打log,分析log,單元測試基本上不靠譜,也可能是我沒有找到合適的方法。
最後採用哪個測試工具,其實微軟測試框架、gtest、qt測試框架基本上都差不多,無非是測試方向,平台,測試assert的表達能力。我在windows下編碼較多,而且使用visual studio,我使用微軟的測試框架,能夠可視化的看到代碼覆蓋情況。
最後的最後,使用了單元測試,你就很容易的使用持續集成這種神器。
可以看下CDash對持續集成的支持 CDash - Continuous Integration Made Easy
Qt的持續集成 Qt Metrics
以前名字叫hudson的持續集成工具 Home - Jenkins


單元測試工具很簡單。其實就是更你寫的test.h的升級版。可以把很多測試腳本管理起來,自動一鍵執行。把錯誤統一格式輸出。

這樣一來,對當前的工作是檢驗,對過去的工作是保障,甚至對未來的工作還有啟發作用。工作效率自然就提高了。

有興趣你也可以做一個xUnit的輪子。


在網上看到單元測試貌似很高大上的樣子,於是內部也想去嘗試一下。
開發的語言為C,我們打算用gtest,然後我們安排了一個編碼能力比較好的跟一個開發進行配對。
整個過程大概就是測試跟開發一起討論需求,設計的方案,開發編碼的時候測試也開始根據設計文檔提供的介面以及自己跟開發每天的溝通來編寫單元測試用例和代碼,然後持續了半個多月的時間,發現了三個bug,而且過程中也碰到了很多問題(比如:開發覺得我們測試浪費了他的時間等),最終放棄了。
說說整個過程中碰到的問題吧!
1,測試認為還是存在能力不足的情況,雖然選擇的人有豐富的自動化開發經驗,但是對C語言還是花了很多時間學習。
2,開發的編碼跟原來一樣設計方案相差比較大,導致單元測試用例變化很大,成本增加很多
3,雖然每天溝通,但是測試還是對開發的很多代碼邏輯不熟悉,增加了單元測試的難度。
4,開發的代碼每次改動,都需要花更多的時間去改動測試代碼,成本太高,
這個也是導致我們取消的原因。

給後面想再去嘗試單元測試的同學們幾個建議:
1、筆者還是覺得單元測試適合開發自己來做。畢竟自己對自己的設計思路和代碼更加熟悉,測試可以去協助和推動開發去做,效果可能更好點。所以,後面我們開始去轉向做介面測試了。
2、做單元測試的人員一定需要有過對應語言的編碼經驗,並且越熟悉越好,這樣雙方配合起來會更加順利點
3、開發的設計文檔要非常完善,這樣單元測試用例才能將其作為一個參考,否則測試代碼改動會比較大,而且跟開發溝通比較難

不知道這樣會不會打擊到那些想做單元測試的朋友,因為筆者以前就一直想去做白盒測試的。


介紹怎麼做單元測試的書很多,這裡主要解答:為什麼單元測試。

客觀來說,單元測試和使用版本控制系統(GIT,SVN)是一樣重要的。

為什麼單元測試如此重要,但你卻感受不到。

首先要知道,代碼的終極目標有兩個,第一個是實現需求,第二個是提高代碼質量和可維護性。
單元測試是為了提高代碼質量和可維護性,是實現代碼的第二個目標的一種方法。
(註:代碼的可維護性是指增加一個新功能,或改變現有功能的成本,成本越低,可維護性即越高。)

—————————————————場景1:Hello World—————————————————
任何一個偉大的程序員都是從最簡單的代碼開始寫起的,假設你的第一個程序是Hello World,任何一個語言實現這個程序都只需要不到5行代碼。

這個程序需要單元測試嗎?,我們看看這個程序是否實現了軟體的兩個目標:
1.需求很簡單,輸出Hello World,這個程序完全滿足需求。
2.只有5行代碼的「軟體」無論是代碼質量,還是可維護性,都相當高,你想要把Hello改成Hi真的很輕鬆。
既然我們已經實現了代碼的目標,要不要使用單元測試是無所謂的,同樣這麼簡單的代碼也沒人會使用GIT或SVN。
代碼量:5行

—————————————————場景2:簡單計算器—————————————————
接下來你寫了一個相對更複雜的程序,一個簡單計算器。
這個程序實現了數字的加減乘除,整個程序共寫了大概50行代碼。

這個程序需要單元測試嗎?
1.需求是對數字進行加減乘除,這個程序滿足了需求。
2.你的代碼風格很好(你已經了解到代碼風格很重要),你使用了縮進,良好的變數命名,邏輯也清晰,代碼的質量和可維護性仍然相當高,如果你想增加一個「求x的平方」功能,你輕而易舉就可以做到。
這個時候讓你去寫單元測試,你仍然會覺得那純粹是浪費時間。
代碼量:50行

—————————————————場景3:圖書管理系統————————————————
你想要做一個真正的實用系統,給學校開發一個圖書管理系統。
你相信這個系統的代碼量比起計算器會很多(可能會有1000行)。
你從書上看到有這樣一些方法可以簡化你的開發工作:
1.工具庫(類似你家裡的工具箱),使用工具庫帶來的好處是非常明顯的,假如你要實現「返回一個數字數組中的最大值」,你只需要使用某個工具庫的Max()函數,只需要1行代碼,而不是10行代碼自己實現。
2.MVC框架,雖然比起工具庫更複雜,需要花更多時間學習,但MVC框架帶來的好處也非常明顯,輕而易舉調用資料庫(Model),實現簡單的UI界面(View),實現了類似「書名為空的書不允許添加到資料庫」的一些邏輯(Controller)。
你最終很好的實現了這個系統,基於MVC模型,你的代碼被很好的分割成了很多小的獨立的模塊:4個Controller,2個Model,4個View。並且在工具庫的幫助下,代碼量得到了縮減,每個模塊大概只有50行代碼(等同於一個簡單計算器的代碼量)。

這個系統需要單元測試嗎?
1.你實現了對圖書的添加、刪除、修改、借閱,你很好的滿足了需求(校長表揚了你)。
2.得益於框架與庫的使用,你的代碼被很好的模塊化了,每個模塊都像一個「簡單計算器」那樣簡單,增加新功能,或修改現有功能似乎也沒有什麼大麻煩,雖然會出現一些小bug,但很快就修復了,代碼質量和可維護性都比較高。
既然你又實現了代碼的目標——「完成需求,高代碼質量和可維護性」,那好像也沒「單元測試」什麼事,畢竟寫它要浪費額外的功夫,而且也沒感覺到有多少好處。
代碼量:500行

————————————————場景3:大型庫存管理系統———————————————
你被一家IT公司僱傭了,你通過了面試,進入了一個即將開啟的項目——為一家大的電商公司做一個庫存管理系統。
項目初期一切都很順利,技術上和你做過的圖書管理系統差不多。
首先你了解了客戶的需求,然後根據他們的需求,使用你已經掌握的MVC框架和一些庫,實現了他們的需求。你寫了30個Controller, 50個Model,50個View,每個模塊的代碼都達到了大概150行,總代碼達到了驚人的20000行!
你覺得自己很了不起,能hold住這麼多代碼,這完全是得益於你的高智商,以及工作努力。客戶很滿意,老闆也很滿意,你的自我感覺也很不錯。

並且你發現了比單元測試更好的東西,面向對象編程(OOP),或函數式編程(FP),無論是哪一種,你發現你可以把一個模塊里的150行堆砌在一起的代碼再提取成1個對象的15種方法,或者15個獨立的函數(具體怎麼提取,你得看相關的書籍),OOP或FP像MVC模型一樣,成功的把你的代碼分割成了更小的組成部分,每個方法或函數里代碼都只有10行左右,你幾乎回到了「Hello World」時代。

你需要單元測試嗎?(你能保證你的系統沒有BUG嗎?)

這個複雜系統是由1950個函數和方法組成,如果想要確定系統整體沒有BUG,就等同於確定組成這個系統的1950個函數和方法沒有BUG。
而單元測試就是做這個事情的,顯而易見,如果你寫了單元測試,並且每個函數都通過了,你就可以驕傲的說:這個系統沒有BUG!(當然這是代碼的角度,而非功能和產品的角度)

-——————————————————-—結論—————————————————————

雖然,從絕對的角度說,單元測試很重要,但是,從相對的角度來講,小的代碼量,簡單固定的需求,個人開發,一鎚子買賣等等都會讓單元測試顯得不那麼重要,並且你一直開發的很舒服,這就是為什麼有的人感受不到單元測試的重要性(這種情況下的確也許不用寫單元測試)。記住,單元測試的威力更多不是體現在新代碼的編寫上,而是對已有代碼的更改。但程序員的智慧是有限的,系統的複雜度卻是無限的,隨著更大挑戰的到來,當系統的複雜度超過了你的邏輯,記憶能力,你必須依靠別的工具來幫助你減少問題。(宇宙中最複雜的系統就是宇宙本身了,假設宇宙是上帝寫的系統,上帝可能太聰明了,所以可能沒寫單元測試,雖然你也是你軟體系統的創建者,但你不是上帝)

如果你現在在做一個較大的項目,這個項目的需求很多,所以你一直在開發,你遇到了這樣的痛苦狀況:1.客戶總能在使用中找出BUG,2.每次代碼的改動,都會導致一些意想不到的BUG出現。這個時候,單元測試可以挽救你。

-——————————————————-—問答—————————————————————
即使我看了單元測試的書,也一頭霧水,不知道怎麼測試我的系統:

這種情況可能是你代碼本身導致的,首先你要寫具有「可測性」的代碼,這意味著你不能寫面向過程的,流水式的,幾百行邏輯堆一起的代碼(也叫義大利面代碼,就像一盤義大利面一樣攪在一起的代碼。),你要學一些模塊化技巧,面向對象函數式編程理念,還有很多其它具體方法,比如能用本地變數,就不要用全局變數等等,讓你的代碼具有可測性,這些知識的學習應該放在單元測試之前。

單元測試代碼比功能代碼也多,這樣成本很高:

事實上單元測試代碼都是異常簡單的一些「斷言」代碼,斷言就是判斷一個函數或對象的一個方法所產生的結果是否等於你期望的那個結果,這樣的代碼看起來很多,但事實上書寫的成本很低。(因為我們開發軟體的大部分時間用在了思考上,而不是敲代碼上,單元測試的代碼邏輯很簡單,不需要太多思考)。

不是有UI界面嗎,點來點去就可以測試了啊:

你完全可以這樣做,直到你覺得這麼枯燥的事情真的應該交給電腦去做,或者功能越來越多,你只點擊你認為影響到的功能,但總會有那些你認為不會影響到的功能也被影響了,你又懶得全部點一遍,單元測試是在你每次改完代碼後自動執行,獲得反饋只要幾秒,並且會把所有功能跑一遍。

我們項目有專職測試人員啊,寫單元測試的必要還大嗎:

單元測試是檢查代碼粒度的bug(一般是以函數和對象的方法為粒度),你可以依賴測試人員,但如果你不想在修改自己一個月前寫的代碼時自己把自己弄到吐血(或者把別人弄到吐血),最好在當初就寫好測試代碼,這個工作的責任完全屬於程序員。外國已經有很多資深程序員論證了,不論你的單元測試代碼質量有多高,覆蓋面有多全,單單是你去做這一件事,就可以很大程度的提高你的功能代碼的質量,以及大幅減少BUG的存在。


我就想說一下,得票最高的 @AAA Yan 的回答根本不是單元測試!根本不是單元測試!根本不是單元測試!


[30天快速上手TDD]序章
微軟mvp寫的文,基於visual studio來做的,裡面的做法或者說思路用在其他的工具上其實也不錯。


其實非常簡單。比如,你寫了一個庫,裡面有一個函數:

float add(float a, float b);

它應當怎樣工作呢?給兩個數,得出相加的結果。
那麼,它首先應當能夠正確處理常用輸入。
然後,測試一下溢出時是否正確反應。
然後,測試一下輸入有NaN、Inf時是否正確反應。
……


單元測試其實本身最重要的不是測試的那個階段,而是你代碼最初設計結構的那個階段。

很多設計不能說不好,但結果是literally不可能做單元測試。

總結一下就是如果你做單元測試,那麼這個模塊是不可以有dependency的,不然的話,fail了算誰的?因為別人的模塊而fail,你就有cover不到的地方。更重要的是,你沒法用mock去return一切可能的返回值,從而去測試你所有的分支。

我個人的習慣是,不一定最好,僅僅是個人的習慣就是所有的dependency都用shared ptr傳進去,這樣你可以全部mock up,做到沒有dependency,你也可以用ref傳,shared ptr省心一點。但很多時候也很難做到,特別是dependency來自別的組。

至於為什麼要做單元測試。同理測試我覺得不是重點。我喜歡寫一個模塊就趕緊編譯跑一跑,不對趕緊改,錯不會犯太遠。

而大多時候你做的東西我們姑且不說編譯時間,舉個伺服器的例子,你還要deploy,bounce,然後發請求,或者一系列請求,然後要在龐大的log里找你需要的東西。如果你做錯了,上面的東西重來一遍。而且很多時候小模塊是沒法獨立去這麼跑的。單元測試所有的log都直接print到console上,比你去查搜方便多了。

單元測試對我來說其實就是簡單的編譯跑跑看。這也是我個人對TDD的理解。

而如果你未來有機會去用python這種沒有typechecking的語言的組,那好好寫單元測試就會至關重要,否則的話等待你的不會僅僅是bug,更多的可能是直接崩。

你要這麼想,測試你是逃不掉的。你要麼在dev上測試,要麼直接在prod上測試。你總要測試,也總要去花時間解決那些你犯的錯誤。而前者的損傷要小的多得多得多。


就是對代碼的基本邏輯單元進行測試,一般是函數、類。
最早剛學編程的時候,其實就會在寫完一個函數或者一個類的時候再在main函數里寫一小段代碼測試下功能;其實這就是單元測試。

但是這樣做畢竟不方便,因為main函數里畢竟本來是寫程序邏輯的代碼,不可能每次完成一個小函數、小類就去把main裡面原來的功能注釋掉,然後寫上你的測試代碼,然後完了再恢復(我大學時就這麼乾的。。。),多蛋疼啊。
而且這樣每次只能測試一個單元的代碼,效率多低啊。
雖然可以設計的好一點,比如在主函數里調用一個單元測試執行的入口,然後把所有的單元測試代碼組織起來都通過那個入口調用來執行,然後輸出結果(這件事做下去就變成寫一個單元測試框架了),但多麻煩啊。
當然現在的IDE都可以創建單元測試的工程,把單元測試的工程和你的項目工程關聯起來,然後單元測試工程里每一個函數就對應一個單元測試,運行的時候每個函數都會被執行一遍,然後輸出結果,多方便啊。


2016-04-13
我以前看過一系列關於用 Boost.Test 做單元測試的博客:
C++ Unit Tests With Boost.Test, Part 1
C++ Unit Tests With Boost.Test, Part 2
https://legalizeadulthood.wordpress.com/2009/07/05/c-unit-tests-with-boost-test-part-3/
C++ Unit Tests With Boost.Test, Part 4
C++ Unit Tests With Boost.Test, Part 5
這一系列總共 5 篇文章。按著教程走一遍,大致就體會到 TDD 的含義了。

2016-06-25T17:28+08:00
請問高手大牛們都是怎麼進行UnitTest的? - C++
----


JUnit,去用用就知道了


C++單元測試入門


我覺得主要還是看你的需求,就像之前我還只是自己寫一些Demo玩的時候(也就是不用維護),覺得單元測試這個東西實在太麻煩了,大大拖慢了開發的速度,但是當我開始做項目的時候(意味著我需要對它進行維護了),而且一般對外的介面是不會輕易改動的,也就是說你代碼的改動對於用api的人來說是完全透明,那麼當你代碼改動後,你如何確保它還能正常工作呢?沒錯,你可以用postman來測試介面,但這意味著你正在做著大量重複的工作,效率極低,如果改動頻繁,介面又多的話,測試一趟肯定要花費大量的時間。這時候自然而然地,你就會想到如何"偷懶",答案就是使用單元測試,在每次改動代碼後跑一遍,確保介面狀況是正常的,這樣就大大節省了你的時間。沒錯,相比印象中粒度到方法的單元測試,我目前單元測試的粒度比較粗,直接到了對外介面這個層面,不過目前來說, 整個項目的後端都是我負責的,我覺得這樣已經足夠了。如果你的項目是由多個分模塊,由多個人來完成的,每個人都有責任維護自己編寫的代碼,比如作為編寫工具類的人就需要維護他為客戶端代碼程序員提供的方法,於是這個時候單元測試的粒度又不一樣了。最後總結一句就是,一切方案基於場景


單元測試幫助開發人員提早發現問題

個人一直粗淺得認為:即使周圍的人都後補單元測試,也好過不寫。


某個功能因為某次commit掛掉,越早發現成本越低。


單元測試使得我們可以放心修改、重構業務代碼,而不用擔心修改某處代碼後帶來的副作用。
單元測試可以幫助我們反思模塊劃分的合理性,如果一個單元測試寫得邏輯非常複雜、或者說一個函數複雜到無法寫單測,那就說明模塊的抽象有問題。
單元測試使得系統具備更好的可維護性、具備更好的可讀性;對於團隊的新人來說,閱讀系統代碼可以從單元測試入手,一點點開始後熟悉系統的邏輯。

摘自剛出的一篇文章:聊聊單元測試


關於單測知識的梳理,可參考這篇文章: 深入探究單元測試編寫 - 琴水玉 - 博客園
如果想知道單測是什麼,請參考單測入門必知必會書籍:《 單元測試之道Java版 (豆瓣) 》 很薄,但是很實在。


假設現在我設計了一個桶排序的 class,那麼我要保證每個 double value 的插入是有序的,於是我研發了這個方法:

/** Returns an index of the Bucket chain into which the specified value shall be inserted */
int get_index(double value) {
if (value &> 1.00 || value &< 0.00) return -1; // mInterval is a double, too return (int)(value / mInterval); }

如果沒有單元測試,我還真沒發現這個方法有個大 BUG!
單元測試就是在 method/api 級別的,保證每一個功能行為的完整性、無歧義、臨界輸入都有處理的測試手段。
單元測試幫助我定位、並解決了 BUG

/** Returns an index of the Bucket chain into which the specified value shall be inserted */
int get_index(double value) {
if (value &> 1.00 || value &< 0.00) return -1; // mInterval is a double, too // mAmplifier = 100,000,000, expand precision to 9 digits return (int)((value * mAmplifier) / (mInterval * mAmplifier)); }


單元測試的難度相當高,各種mock超級複雜,還有嚴重的侵入性(源代碼里到處加宏,說的是c,java稍微好點),而且不可能做全,最多調幾個主要的函數忽悠下領導,一些上層函數,扇出大的函數,基本無法寫ut,根本無法構造出運行它的上下文,在我看來,ut就是軟體界的政治正確,測代碼,還是黑盒測試,最上面寫幾個main函數,分功能總體調一下靠譜


推薦閱讀:

怎麼理解API和MFC的關係?
C++ 中為什麼要有. -> ::這幾種成員訪問操作符?
面試 C++ 程序員,什麼樣的問題是好問題?
發現很多外掛和木馬編寫都是用MFC,MFC有必要學嗎?
如何看待阿里2016校招研發工程師筆試題題目?

TAG:編程 | 敏捷開發 | C | 單元測試 | 測試驅動開發 |