嵌入C語(yǔ)言的測試驅動(dòng)開(kāi)發(fā):為什么要調試?
要點(diǎn)
本文引用地址:http://dyxdggzs.com/article/201706/347781.htm1.為什么你會(huì )遇上這些bug?因為它們是你放的。
2.在TDD(測試驅動(dòng)的開(kāi)發(fā))中,你會(huì )在一個(gè)嚴格的反饋循環(huán)中,開(kāi)發(fā)測試與生產(chǎn)代碼。
3.TDD可能有助于避免惱人的Zune bug。
4.目標硬件瓶頸有多種形式,你可以在嚴格的TDD反饋循環(huán)中,用TDD來(lái)避開(kāi)瓶頸。
5.TDD幫助你確保自己的代碼如期望那樣運行。但如果不是這樣,你該如何建立一個(gè)可靠的系統?
6.TDD快速地發(fā)現小的和大的邏輯錯誤,防止出現bug,使最終得到較少的bug。
我們的工作方式都是編寫(xiě)代碼,然后努力讓它運行起來(lái)。先建立,然后改錯。測試是以后的事,即寫(xiě)完代碼后才要做的事。在不可預期的調試工作上,大概要花掉我們一半的時(shí)間。在日程表上,調試工作都穿著(zhù)測試與集成的外衣。它是風(fēng)險與不確定性的一個(gè)來(lái)源。修正了一個(gè)bug可能會(huì )產(chǎn)生另一個(gè)bug,有時(shí)甚至是一連串的bug。
保持調試的統計有助于預測要花多少時(shí)間才能消除bug。你要度量和管理bug??辞€(xiàn)的拐點(diǎn),拐點(diǎn)表示了趨勢,告訴你最后修正的bug要比產(chǎn)生的多。拐點(diǎn)表示的是已經(jīng)做的事,但你永遠不知道是否在代碼的某個(gè)陰暗角落還躲藏著(zhù)其它的致命bug。
可制造性設計的一個(gè)方面是確定為什么你會(huì )有這些bug。答案很簡(jiǎn)單:錯誤是我們放進(jìn)去的。這就是我們的工作方式。在開(kāi)發(fā)以后的測試時(shí),就會(huì )發(fā)現問(wèn)題(圖1和參考文獻1)。我們在開(kāi)發(fā)時(shí)會(huì )制造錯誤,測試的工作就是找到這些問(wèn)題。只要仔細地測試,就會(huì )發(fā)現錯誤。開(kāi)發(fā)后的測試工作意味著(zhù)必須找到、修復和管理大量的錯誤。
圖1,在開(kāi)發(fā)以后做測試時(shí),會(huì )發(fā)現缺陷
這種調試居后的編程程序是當今最常見(jiàn)的編程方式。先寫(xiě)代碼,再調試它。調試居后的編程方式有風(fēng)險。人都會(huì )犯錯誤。你既不能確定bug將在何時(shí)現身,也不能確定會(huì )花多長(cháng)時(shí)間才能發(fā)現它們(圖2)。
圖2,人都會(huì )犯錯誤。你無(wú)法確定bug何時(shí)出現,以及要花多少時(shí)間才能找到它們
當發(fā)現一個(gè)bug的時(shí)間(TD)增加時(shí),尋找bug根源的時(shí)間(TFIND)也會(huì )增加,通常增加得更多。如果從錯誤的引入到發(fā)現要花數小時(shí)、數天、數周,甚至數月時(shí)間,你已忘掉了當時(shí)的背景,必須開(kāi)始做bug大掃蕩。當你在開(kāi)發(fā)周期以外發(fā)現缺陷時(shí),就必須管理bug。對于有些bug,發(fā)現的時(shí)間不會(huì )影響修復的時(shí)間(TFIX),但有些代碼的運行也可能依賴(lài)于bug,修改這些bug會(huì )造成其它bug。
短周期以及主動(dòng)的測試自動(dòng)化可節省時(shí)間和工作量。這時(shí),你再不需要重復繁重而易錯的手工測試。有了測試自動(dòng)化,重復測試幾乎不會(huì )增加額外工作量。測試自動(dòng)化快速地探測出副作用,避免了對調試事務(wù)的需求。
另一種方案是TDD(測試驅動(dòng)的開(kāi)發(fā)),它在一個(gè)嚴格反饋的循環(huán)中開(kāi)發(fā)出測試代碼與生產(chǎn)代碼(參考文獻2和3)。一個(gè)TDD微循環(huán)是:編寫(xiě)一個(gè)測試,未編譯時(shí)觀(guān)察該測試,做編譯且測試失敗,使編譯通過(guò),清除任何多余內容,并重復該過(guò)程直至結束。編寫(xiě)測試代碼與編寫(xiě)生產(chǎn)代碼是整合的過(guò)程。如果犯了一個(gè)錯誤,沒(méi)有通過(guò)新測試,你馬上就可以知道并改正錯誤。測試會(huì )告訴你是否通過(guò)了新測試卻產(chǎn)生了某個(gè)錯誤。在設備測試裝置中加入自動(dòng)化測試(圖3),就可以自由地做重復測試。
圖3,測試會(huì )告訴你是否通過(guò)了新的測試,但卻引入了一個(gè)bug。自動(dòng)測試要插入到一個(gè)單元測試裝置中
在TDD反饋回路中做開(kāi)發(fā)與測試時(shí),只能避免一部分bug的出現,但不能完全消除。TDD對設計以及時(shí)間的分配方式有著(zhù)意義深遠的影響。
與后調試的編程模式相反,TDD并不包含追蹤錯誤的風(fēng)險與不確定性(圖4)。當發(fā)現一個(gè)錯誤的時(shí)間接近于0時(shí),尋找錯誤根源的時(shí)間也會(huì )趨于0。剛產(chǎn)生的代碼問(wèn)題通常顯而易見(jiàn)。如果不那么明顯,則開(kāi)發(fā)人員只要簡(jiǎn)單地恢復剛做的修改,就可以回到一個(gè)可運行的系統。尋找和修改錯誤的時(shí)間和產(chǎn)生的時(shí)間一樣少,只有當程序員記憶隨時(shí)間而模糊,并且有更多的代碼依賴(lài)于較早的錯誤時(shí),事件才會(huì )變糟。
TDD為錯誤提供了即時(shí)的通知,可防止出現很多要被迫追蹤的bug。TDD可防止出現缺陷,而后調試編程會(huì )帶來(lái)耗時(shí)耗力的調試工作。
Zune bug
TDD可能有助于避免惱人的Zunebug。微軟公司的Zune是為了與蘋(píng)果公司的iPod競爭。2008年12月31日,Zune變成了“專(zhuān)為一天的程序塊(abrick for a day)”。12月31日是新年前夜,是一個(gè)閏年的最后一天,這是30G Zune要經(jīng)歷的第一個(gè)閏年。很多人都將Zune錯誤歸因于時(shí)鐘驅動(dòng)程序中的一個(gè)函數。雖然列表1中的代碼并非實(shí)際的驅動(dòng)程序碼,但它有相同的效果。你可以從列表1中Zune的無(wú)限循環(huán)中找到一些端倪嗎?
圖4,TDD對于設計以及時(shí)間的使用有深遠的影響。與調試居后的編程模式比較,TDD
沒(méi)有回溯追蹤bug的風(fēng)險與不確定性
圖5,對快速反饋的需求使TDD微循環(huán)離開(kāi)目標硬件,而原生地運行在開(kāi)發(fā)系統上。一個(gè)TDD循環(huán)包括雙重目標的風(fēng)險,但提供了快速TDD反饋回路的好處
很多代碼閱讀專(zhuān)家審查了這個(gè)代碼,并得出了可能與您一樣的錯誤結論。閆年的最后一天是該年第366天,而Zune對這種情況的處理是錯誤的。在這一天,該函數永遠不會(huì )返回!我編寫(xiě)了設定年份以及年中天數的代碼,看是否像90%的Zune bug專(zhuān)家預測的那樣,將天數的布爾代碼設定為等于或大于366就能解決問(wèn)題。代碼放入測試裝置后,我編寫(xiě)了測試用例(列表2)。和Zune一樣,測試進(jìn)入了一個(gè)無(wú)限循環(huán)。我采用了經(jīng)過(guò)數千名程序員審核的適當修復方法。出乎我的意料,測試失敗了;設定年份與天數的測試認為日期是2009年1月0日。新年前夜,人們仍會(huì )擁有自己的音樂(lè ),但Zune仍有個(gè)bug。
一次測試就可以防止Zune bug??赡阍趺粗酪?xiě)這樣一個(gè)測試?只有知道bug在哪里才會(huì )寫(xiě)測試。問(wèn)題是,你并不知道bug在哪里;它們可以在任何地方。所以,這意味著(zhù)你必須為所有的部分寫(xiě)測試,至少是所有可能中斷的地方。難以想象要考慮到所有需要測試的東西。但不必擔心,你不需要針對全年每一天做測試。你只需要一個(gè)針對有關(guān)天數的測試。
計算機編程很復雜,TDD能夠系統化地讓你的代碼按本意運行起來(lái),并提供能使代碼工作的自動(dòng)化測試用例。
嵌入設計
當我首次使用TDD時(shí),我認識到,它可能有助于解決一個(gè)問(wèn)題:目標硬件的瓶頸,這是令很多嵌入軟件開(kāi)發(fā)人員頭疼的事情。瓶頸有多種形式,你可以使用TDD,在嚴格的TDD反饋循環(huán)期間避免瓶頸的出現。很多嵌入開(kāi)發(fā)工作都已實(shí)現了軟硬件的并行開(kāi)發(fā)。如果軟件只能在目標硬件上運行,則可能浪費至少一次的時(shí)間。例如,目標硬件可能遲至交付期還不可用,推遲了軟件的測試;硬件可能昂貴且稀少;或者它本身就有問(wèn)題。目標硬件還可能有長(cháng)的建立時(shí)間或長(cháng)的上傳時(shí)間。大多數嵌入開(kāi)發(fā)團隊都遇到過(guò)這些問(wèn)題,它們會(huì )減緩進(jìn)度,并減少了建立今天復雜系統的反饋。
為避免目標硬件的瓶頸,可以采用“雙重目標”法,即設計自己的生產(chǎn)代碼與測試,使之大部分運行在標準PC上。但雙重目標有自己的風(fēng)險。開(kāi)發(fā)系統中測試代碼的信任度是建立在交付給目標以前的代碼上。大多數雙重目標風(fēng)險是源于開(kāi)發(fā)環(huán)境與目標環(huán)境之間的差異。這些差異包括對語(yǔ)言特性支持的改變量、不同編譯器的bug、運行時(shí)庫的差異、文件名差異,以及不同的字長(cháng)等。由于這些風(fēng)險,你會(huì )發(fā)現,在一個(gè)環(huán)境下能無(wú)錯運行的代碼,可能在另一個(gè)環(huán)境下出現測試錯誤。
不過(guò),執行環(huán)境中潛在的差異不應成為阻礙采用雙重目標方法的理由。相反,你可以在實(shí)現目標的路途中解決這些障礙。嵌入TDD周期在不犧牲優(yōu)點(diǎn)的前提下,克服了挑戰。
開(kāi)發(fā)循環(huán)
當建立與測試循環(huán)只需幾秒時(shí)間時(shí),TDD是最有效的。這種方案為大多數程序員排除了在循環(huán)中使用目標硬件的情況??焖俜答伒男枨髮DD微循環(huán)與目標分離開(kāi),而運行在開(kāi)發(fā)系統上。圖5顯示了一個(gè)TDD循環(huán),它包含著(zhù)雙重目標的風(fēng)險,提供了快速TDD反饋循環(huán)的好處。
表1中所列的各個(gè)階段,預計可以在相應的階段發(fā)現問(wèn)題。例如,你會(huì )發(fā)現每個(gè)階段都有助于找到這些問(wèn)題。第1階段會(huì )在你編程時(shí)給出快速反饋,確定代碼做你想要做的事。第2階段確保你的代碼是在兩種環(huán)境下編譯。第3階段確保代碼在主處理器和目標處理器上的運行相同。評估硬件可能需要比目標更多的存儲器,這樣才能把測試代碼和生產(chǎn)代碼都裝入地址空間。有時(shí)候,如果你有一個(gè)可靠的目標硬件,它有空間運行對單元的測試,也可以省略掉第3階段。第4階段是在目標硬件上運行測試。在第4階段可以引入一些依賴(lài)于硬件的單元測試。第5階段是看你的系統完全整合時(shí),是否如其應該的那樣運行。至少讓第5階段的某些部分自動(dòng)運行,這是一種好的想法。采用TDD的團隊會(huì )發(fā)現第1階段中的巨大價(jià)值,可能不要實(shí)現全部各個(gè)階段。
嵌入TDD循環(huán)并不能阻止所有問(wèn)題,不過(guò)它應有助于在適當的階段發(fā)現大多數剛剛產(chǎn)生的問(wèn)題。你還應至少每個(gè)夜晚手動(dòng)執行第2至第4階段。連續的集成服務(wù)器(如Cruise Control或Jenkins)都可以觀(guān)察你的源碼庫,在check-in后開(kāi)始做建立工作。
TDD有助于確保你的代碼做你想要做的事。如果不是這樣,如何才能建立一個(gè)可靠的系統呢?它幫助你讓代碼在最開(kāi)始時(shí)保持正確,它建立一個(gè)逐步測試的組件,幫助你維持代碼的運行。你在發(fā)現、追蹤和修改bug上要花掉相當多的時(shí)間。很多開(kāi)發(fā)人員現在都用TDD來(lái)防止這些bug的出現。它基本上改變了你的編程方式。
TDD能快速地發(fā)現小的和大的邏輯錯誤,阻止bug的產(chǎn)生,并最終得到較少的bug。較少的bug也意味著(zhù)較少的調試時(shí)間,以及較少的缺陷。當新代碼危及一個(gè)約束或一個(gè)假設時(shí),測試會(huì )告訴你。然后,有良好結構的測試會(huì )成為一種形式的可執行文檔。
TDD還讓你放心,這種信心來(lái)自于一個(gè)帶有完備回歸測試組件的徹底測試代碼。采用TDD的開(kāi)發(fā)人員稱(chēng)周末不再受干擾,并且睡眠更好。TDD還監控進(jìn)度,追蹤當前的工作,以及做了多少工作。當代碼變得難以測試時(shí),它還對設計問(wèn)題提出早期警告。
評論