不好意思,請問大家什么是編程?編程是用來干什么的?別笑我好嗎?
熱心網友
編程就是用特定的語言編寫程序,我們現在用的一切電腦程序都是編程做出來的,懂了嗎
熱心網友
最近 50 年來,測試一直被視為項目結束時要做的事。當然,可以在項目進行之中結合測試,測試通常并不是在所有編碼工作結束后才開始,而是一般在稍后階段進行測試。然而,XP 的提倡者建議完全逆轉這個模型。作為一名程序員,應該在編寫代碼之前編寫測試,然后只編寫足以讓測試通過的代碼即可。這樣做將有助于使您的系統盡可能的簡單。先編寫測試XP 涉及兩種測試:程序員測試和客戶測試。測試驅動的編程(也稱為測試為先編程)最常指第一種測試,至少我使用這個術語時是這樣。測試驅動的編程是讓程序員測試(即單元測試 — 重申一下,只是換用一個術語)決定您所編寫的代碼。這意味著您必須在編寫代碼之前進行測試。測試指出您需要編寫的代碼,從而也決定了您要編寫的代碼。您只需編寫足夠通過測試的代碼即可 — 不用多,也不用少。XP 規則很簡單:如果不進行程序員測試,則您不知道要編寫什么代碼,所以您不會去編寫任何代碼。如何先編寫測試整個理論很棒,但如何先編寫測試呢?首先,我推薦您閱讀 Kent Beck 撰寫的 Test-Driven Development: By Example(請參閱參考資料)一書,里面列舉了一個詳盡的貫穿于整本書的示例。該書不僅講述了如何編寫測試和讓這些測試來驅動您的代碼的原理,而且還講述了測試驅動的編程為什么是一種好的編程方法。這里我將舉一個簡單的例子,讓您體會一下我正在講什么。 測試驅動 vs。 先測試我喜歡用“測試驅動”這個術語,而不喜歡用“先測試”這個術語,因為先測試強調了在編寫代碼前編寫程序員測試這個原理。這些原理很重要,但真正的力量在于測試驅動所隱含的想法和編程習慣的改變。“測試驅動的編程”這一更貼切的術語包含兩種測試,它指出 XP 團隊強調讓測試驅動他們所要做的一切這一方式。 假定我正在編寫包含 Person 對象的系統。我希望在我問每個 Person 時,他/她能告訴我其年齡(作為整數)。即使我還沒有編寫一丁點代碼,但也該編寫測試了。“什么?”,您可能會說,“我甚至不知道在測試什么,怎么編寫測試?”答案很簡單,您的確知道您在測試什么,只是不知道您所了解的內容,因為您不習慣按這樣的方式進行思考。這就是我的意思。您確實還沒有任何代碼,但您腦海中應有 Person 對象的雛形。Person 對象上應該有一個方法,該方法可以用整數形式返回年齡。因為我最常使用 Java 語言,所以我用 JUnit 來編寫程序員測試。清單 1 顯示了我為 Person 對象編寫的 JUnit 測試:清單 1。 用于 Person 對象的 JUnit 測試package ywmiller。testexample;import amework。TestCase;public class TC_Person extends TestCase {protected Person person;public TC_Person(String name) {super(name);}protected void setUp() throws Exception {person = new Person();}public void testGetAge() {int actual = tAge();assertEquals(0, actual);}protected void tearDown() throws Exception {}} 首先,讓我向那些不熟悉 JUnit 的人講述一些淺顯的原理。TestCase 類是您將最常使用的類。您只是寫了一個測試類(在該示例是 TC_Person),它是 TestCase 的子類。(注:在 JUnit 3。8。1 中,可以有也可以沒有接受 String 的構造函數,但由于我幾乎所有的 Java 開發都在 Eclipse IDE(請參閱參考資料)中完成,Eclipse IDE 免費向我提供了這個構造函數,所以我就把它保留在這里了。)一旦創建好測試類之后,測試方法中要有實際的動作。這些方法都恰如其分地用前綴 test 開頭(它們必須是 public,并且返回 void)。當運行測試時,JUnit:內省測試類,并執行每個以“test”開頭的方法 在執行每個測試方法之前執行 setUp() 方法 在執行每個測試方法之后執行 tearDown() 方法 在該示例中,setUp() 方法中沒有太多要執行的語句。它只是實例化 Person(我用這個方法是讓您覺得這個測試案例看上去很“完整”)。這意味著,如果這里有 20 個測試方法,則每個測試方法都以一個新的 Person 實例開始。tearDown() 中不做任何事情,所以現在它是空的。值得強調的一點,您不需要 setUp() 或 tearDown();我通常直到編寫第二個或第三個測試方法,并確定了這些方法都共享某些公共的設置或銷毀活動時,才創建它們。有了這些原理之后,要注意,我在測試方法中制訂了一些設計決策。我假定,可以構造一個 person,并且“缺省”Person 會返回值為 0 的 age。還假定 Person 對象有 getAge() 方法。即使那些假定不會一直都成立,但目前它們還適用。可以說,這是一個簡單的測試,讓我說明測試驅動的編程。有了這些假定之后,實例化 Person(在 setUp() 中實例化 Person 只是為了展示如何使用 setUp() 方法),接著調用測試方法中正在測試的方法,然后調用其中一種“斷言(assert)”方法。斷言方法測試事情是否為 true。換句話說,這些方法針對某件事做出一個斷言,該斷言告訴 JUnit 驗證該事是否為 true。表 1 列出了斷言的類別:表 1。 斷言類別 斷言方法 描述 assertEquals 比較兩件事物是否相等(基本類型或對象) assertTrue 對布爾值求值,看它是否為 true assertFalse 對布爾值求值,看它是否為 false assertNull 檢查對象是否為 null assertNotNull 檢查對象是否不為 null assertSame 檢查兩個對象是否為同一實例 assertNotSame 檢查兩個對象是否不為同一實例 在這里,我檢查 Person 實例的 age 是否為 0,新 Person 對象的缺省值為 0。當然,這個測試甚至不能編譯。圖 1 顯示了當我試圖在 Eclipse 上運行它時的 JUnit Fast View。圖 1。 JUnit 編譯失敗顯然,我還沒有 Person 類,所以運行該測試會出現問題 — JUnit 給出了一個紅條。如果可以運行,并通過測試,則會顯示一個綠條。您的目標總是設法得到一個綠條。別忘了,JUnit 的座佑銘是“得到綠條,使代碼干凈”(有時抱怨是難免的)。沒問題。我將創建 Person 類,如清單 2 所示:清單 2。 Person 類package ywmiller。testexample;public class Person {public int getAge() {return 0;}} 現在,當運行這個測試時,測試通過,應該可以看到一個綠條。我必須從 getAge() 返回值,否則不會編譯它。這里碰巧 0 最方便,0 被認為是新的 Person 實例的缺省值,所以工作正常。再次重申,我只編寫了通過測試所需的代碼。能夠使 Person 具有缺省的年齡值固然很好,但這對我的系統不會有太大幫助。Person 需要比這更智能些。我真正所需要的是,Person 擁有其生日,并能回答其當前的年齡。這意味著 Person 對象的年齡會隨時間的推移而增長。在進行編碼前,將 testGetAge 重命名為 testGetDefaultAge(清楚地表明,我正在測試缺省的年齡),并為這個測試案例編寫另一個測試方法,如清單 3 所示:清單 3。 新的測試方法public void testGetAge() {GregorianCalendar calendar = new GregorianCalendar(1971, 3, 23); tBirthDate( tTime());int actual = tAge();assertEquals(31, actual);} 還不能編譯這個測試(您注意到了其中的模式嗎?),因為 Person 內沒有 setBirthDate() 方法。在創建了這個方法之后,Person 將類似于清單 4 所示:清單 4。 更新的 Person 類package ywmiller。testexample;import java。util。Date;public class Person {protected Date birthdate;public int getAge() {return 0;}public void setBirthDate(Date aBirthDate) { rthdate = aBirthDate;}} Person 中的 getAge() 仍然沒有什么變化,所以測試失敗。圖 2 顯示了 JUnit Fast View 中出現的信息:圖 2。 JUnit 斷言失敗生成的 AssertionFailedError 告訴我結果不是 31 而是 0。這個失敗在預料之中,因為我沒有改變 getAge() 方法來做某些不同的事。現在僅僅編寫足夠使測試通過的代碼(這里有兩個測試)。我必須允許年齡的缺省值為 0,但我必須計算出生于 1971 年 3 月 23 日的人的年齡。一些程序員(包括 Kent Beck)建議在這一點上盡可能簡單,譬如檢查 birthdate,看它是否為 null — 如果為 null,則返回 0,否則返回 31 — 然后編寫另一個測試使計算更智能。一小步一小步地思考問題這種方法是很好的技術,我們要采用這種技術,當您想回到上面提到的基本規程來使自己擺脫調試慣例時,那是再好不過。但這里我想使該示例略微簡單些,所以我僅僅試圖通過按我所希望的方式,用 Calendar 計算年齡,使該測試通過。清單 5 顯示了 Person 中我所編寫的代碼:清單 tAge()實現package ywmiller。testexample;import java。util。Calendar;import java。util。Date;import java。util。GregorianCalendar;public class Person {protected Date birthdate;public int getAge() {if (birthdate == null)return 0;else {int yearToday = tInstance()。get(Calendar。YEAR);Calendar calendar = new GregorianCalendar(); tTime(birthdate);int birthYear = t(Calendar。YEAR);return yearToday - birthYear; }}public void setBirthDate(Date aBirthDate) { rthdate = aBirthDate;}} 當我運行測試時,我失敗了,預期的結果為 31,但實際結果為 32。怎么了?唔,我知道問題一定出在剛才所寫的代碼中,沒有進一步考慮下去。在檢查完 else 子句之后,我明白我只是根據年來計算年齡。這不對。我現在 31 歲,但這個月再過幾天我要 32 歲了(但我寫該代碼時,是 3 月份),我的算法造成錯誤的結果。所以需要重新考慮 getAge()。我用清單 6 中的代碼段糾正了這個錯誤:清單 6。 改正后的 getAge()else {int yearToday = tInstance()。get(Calendar。YEAR);Calendar calendar = new GregorianCalendar(); tTime(birthdate);int birthYear = t(Calendar。YEAR);if (yearToday == birthYear)return yearToday - birthYear;elsereturn yearToday - birthYear - 1;} 綠條!在 Person 類中有一些重復的代碼,但我把它留給稍后的重構練習。歡迎替我清理該代碼。您可以有信心地做這件事,因為可以運行測試來證實您沒有破壞任何事物。這個示例使您體會到了測試驅動的編程類似于什么。我只在每步編寫足夠讓測試通過的代碼。作為一種理論,這在思想傾向上是一種挑戰。您必須習慣這種思想,在編寫代碼之前,可以并應該編寫測試。在通過所有測試之后,就完成了工作。在先編寫測試時,必須習慣故意只看眼前。清單 6 中的示例是一個十分簡單的情形。即使最簡單的編程問題,在實際當中通常要更復雜。這種方法有助于將問題分解成更可管理的部分,但您最終仍可能遇到一些復雜的令人頭疼的問題。在那些情況下,必須使自己不要考慮太遠,不要假定它的“普適性”有多高,也不要假定這種方法能處理某些尚未遇到的情形。僅僅編寫測試,使它通過。您需要采取一些較小的步驟,然后編寫迫使您要采取更多步驟的測試。請記住,您正在測試代碼的存在性,如果您以較小的步驟來編寫代碼,那您就做對了。為什么應該先編寫測試……也許您不認為先編寫測試是一個好主意。它看上去似乎很奇怪,或者也許似乎沒有必要。我常從富有經驗的程序員那聽到第二種原因。這些程序員很聰明,他們具有許多經驗,他們說不需要先編寫測試,因為他們知道自己在做什么。我能領會,但我懷疑他們存在一個隱式的假定:他們沒能領會先編寫測試。恕我難以茍同。事實上,我認為采用先編寫測試方法有三個原因:學習 新問題 信心 先編寫測試 — 到后面再執行這些測試 — 是較佳的學習方式。它使您能將精力集中在所編寫代碼的接口部分。在編寫測試時,您假設正在使用的類已經存在,然后按照您希望在系統其余部分中使用的方式來使用該類。稍后,當您忘記如何使用該類時,可以查看測試,看一個非常具體的示例。這是學習的很好方式。關于先編寫測試最有趣的事情之一是,它有助于發現新問題。您正在使創建之中的系統“成長”起來。如果您正在使用 XP,則沒有預先設計整個事情 — 而是一邊開發一邊設計。在您先編寫測試并通過測試這個過程中,您正在讓代碼告訴您它想要做什么,以及會成為什么。如果僅僅著手編碼工作,則您完全按照您的設想來行事了。越晚做決定,則越有可能發現新問題和新動向,這些可使您的系統更完善。但是,我所喜歡的先編寫測試的好處是讓這些測試在稍后執行。在我先編寫測試時,我有許多奇異的邏輯。我可能沒有涵蓋代碼的方方面面,但會包括其中許多方面。在任何情況下,我會有一套測試,這套測試比我曾參與過的大多數沒采用 XP 的項目要更好。我可以按一個按鈕就運行這些測試。幾秒種之后,我就知道代碼是否按我告訴它應該怎樣的方式來運行。這種可回歸的工具是很有價值的。我團隊中的任何人(或任何地方的任何人)可以在任何時候更改代碼,甚至在代碼發布的前一天也可以更改,因為如果有任何問題,測試會立即告訴他們。作為一名程序員,這給予了我信心 — 比大多數程序員具有更大的信心。 幫助形成“揭開極端編程的神秘面紗”的未來一如既往,我熱忱邀請您就以后的專欄文章提出您的反饋意見,這樣有助于促進這個專欄。關于 XP,您存在的最大問題是什么?您認為是完全愚蠢的、不明智的、非專業的還是不可能的?最讓您感到迷惑的做法是什么?請在本文的論壇提出您的建議,或者就直接給我發電子郵件。 ……為什么人們沒有采用許多沒有先編寫測試的程序員甚至不知道還可以使用這種方法。如果他們知道,也可能對如何使用它感到迷惑,或者他們可能想知道為什么要這樣做。即使他們知道如何去做,并認為它是一種好的想法,但許多人仍然沒有先編寫測試。先編寫測試需要遵循一定的規程。作為一名程序員,我認為,對于我正在開發的工作,不編寫測試可能會更容易些。有時確實如此,但通常只會在短期內是這樣。如果我經常不寫測試,那么不久會有一堆代碼沒有經過測試。當編寫下一個系統功能部件時,可能會出現不正常現象,問題出在哪里?沒有測試,我無法胸有成竹地回答這個問題。即使一切似乎都工作良好,但我不能確保過去在系統中沒有出現的問題在以后還不會出現。這種惡性循環就是為什么大多數程序員討厭測試人員告訴他們代碼出現問題的原因。在沒有測試的前提下,跟蹤錯誤造成了加班加點以及對工作的不滿意。在我用那種方式向大多數程序員說明這種情況時,他們認為測試驅動的編程是一個不錯的想法 — 這之后,他們仍然不使用這種方法。在編寫代碼前編寫測試這種作法意味著,在測試運行并失敗之前,不會做工作中真正有趣的部分。不要掉入這個陷阱,否則您以后會付出很多。難以處理的情形在人們開始編寫測試時,總是會遇到這樣一些情形:他們說,“只是沒有辦法進行測試”。XP 社區的一些人可能毫不含糊地說,不寫測試就永遠別寫代碼。您應該努力嘗試這么做,但以我個人的經驗,有時我發現有些地方我也不能這么做。如果您發現自己處在這種情形,您應該放棄嗎?在一定程度上可以。我認為您可以做兩件事:在編寫測試前編寫代碼 在很少情況下,根本不編寫測試,放到后面編寫 如果發現在嘗試先編寫測試之后,仍不能先編寫測試,那么回到測試中來。我仍然希望進行測試,這樣我可以從完整的回歸套件中獲得信心,但我必須先編寫一些代碼,然后編寫測試。有時我編寫了一點代碼,然后編寫一點測試,這樣兩者可以一起并進。在少數情況下,我恰好根本想不出如何編寫測試。在出現這種情形并且我的結對搭檔也想不出法子時,我就問問其他人(例如,另一對搭檔),看看他們是否什么聰明點的主意。有時這很管用。但還有一些時候,整個團隊都陷入了困境。在那些情況下,必須選擇可行性。我可能暫停編碼,陷入困境,或者在沒有測試的情形下,編寫一些代碼,到稍后再編寫測試。也許代碼中出現的第一個錯誤會使測試什么以及如何測試變得更為明晰。這些是可行的規則。測試工具和技術在這世上,幾乎每種語言都有一個 xUnit 庫。對于 Java 平臺,則由 JUnit 擔當此任。我個人使用 Eclipse IDE(請參閱參考資料),它極好地集成了 JUnit。Eclipse 是開放源碼,有它自己的測試套件,您可以使用它。使用這個合適工具,您可以編寫大量好的測試。但有時最好有一些其它幫助。幸運的是,可以利用一些編碼技術來更方便地進行測試,甚至可以測試看上去不可測試的事物。可以使用的一些技術包括 ObjectMother 模式、模仿對象(Mock Object)和偽(Sham)對象。ObjectMother 模式ObjectMother 模式實際是 Gang of Four Abstract Factory 模式(請參閱參考資料)的實現,它告訴您創建一個工廠對象來給出需要測試對象的實例。例如,假定您正在構建一個處理客戶預訂講座的系統。您可能構建一個 ObjectMother 對象,使 Seminar 對象具有不同種特征,您可以用這些特征來測試某些情況。在 Java 語言中,您可能創建 TF_Seminar 對象,它有幾個靜態工廠方法 — 也許稱為 createSomething 或 newSomething。用您對正在創建事物的一些描述代替“something”,譬如 newFullyLoaded,用它來創建具有所有數據成員、并且這些數據成員都已填有已知數據的 Seminar。這樣做使得測試數據放在一個地方,從而使代碼更干凈,更容易重構。在代碼中,每當需要完全裝入的 Seminar 來進行測試時,可以象清單 7 那樣做:清單 7。 ObjectMother 示例Seminar seminar = TF_ wFullyLoaded; Something();assertEquals("expectedValue", tValue()); 模仿對象模仿對象使您可以為測試而模仿對象(請參閱參考資料)。給模仿對象一個接口,您希望實際組件具備這個接口,然后使用模仿對象,直到實際組件成形。但模仿對象不僅僅只是還未存在的組件的存根。可以評估代碼如何與模仿對象交互(譬如,驗證調用了某個方法多少次以及檢查狀態等)。最近模仿對象得到了大力推廣,但我認為它們被濫用了,它們太“重”以至于不切實際。偽對象有時我所希望的是一個偽對象,它實現了與真實對象相同的接口,可以回答關于我在測試中如何與它交互這樣一些特定問題。這就是偽對象 — 一種用來偽裝測試中對象的輕量型方式。偽對象可以是您所需要的任何對象。它是我曾使用過的最全面靈活的工具、模式和思考方式,我推薦您使用它。例如,我在前面創建的 Person 對象的偽對象類似于清單 8 中的樣子:清單 8。 Person 的偽對象protected class ShamPerson extends Person {protected boolean getAgeWasCalled;public int getAge() {getAgeWasCalled = true;return 25;}}具體請看: 。