InfoQ觀點:測試驅動開發其實是設計技能

本文要點

  • 我們知道,任何軟件都必須通過測試確保其功能符合要求;此外我們還要測試軟件的非功能性指標,如安全性、可用性,尤其是可維護性。
  • 測試驅動開發(TDD)是一種成熟的技術,可以幫助開發者交付更優秀的軟件、縮短交付週期,並使交付週期更加穩定。
  • TDD的理念很簡單:在編寫生產代碼之前先編寫測試代碼。但要實踐好這種“簡單”的理念也需要技巧和判斷力。
  • TDD實際上是一種設計技能。TDD的本質是利用一系列小型測試來從零開始一步步設計系統,並在系統逐漸成型的過程中快速獲得價值。這種技能其實改名叫測試驅動設計更合適些。
  • 一般來說,爲某個問題開發解決方案時,第一步就是不管問題有多複雜,先分析問題的結構並把它拆分成許多小需求。之後這些需求就可以逐步實現,解決每一小步時都要同時考慮輸入和輸出場景。

我們需要測試軟件以確保其滿足要求;軟件要正確響應輸入(輸入驗證),在可接受的時間內完成任務(性能測試),可以被用戶正常安裝和運行(部署測試),還要達成利益相關方的目標。這些目標可能是業務成果或業務功能,諸如安全性、可用性、可維護性等。

測試類型包括:

  • 煙霧測試和可用性測試,檢查軟件的基本功能運行情況。
  • 持續測試,每次迭代時運行,比如運行Maven時做測試。
  • 迴歸測試,每次添加新的編程代碼或改動現有代碼時使用。我們希望通過測試確認其它代碼依舊正常工作。
  • 性能測試,測量軟件完成任務所需的時間。
  • 驗收測試,考察利益相關者是否滿意軟件的表現,是否願意支付費用。

單元測試是一組測試中最小的模塊。編程代碼中的每個類都伴隨有一個單元測試類。可通過模擬方法調用將測試與其它類隔離。

集成測試更容易實現,我們會測試一個包含所有依賴項的類。測試成功的話我們就知道軟件內的路徑是暢通的,但如果測試失敗,我們也沒法判斷到底是哪個類出了問題。系統測試則會檢查整個系統,包括硬件操作系統、Web服務等等。

測試應該是可讀的,以便非程序員閱讀或改動測試內容。在敏捷團隊中,程序員與測試人員和分析人員協同工作,測試和規範則屬於協作內容,因此大家都應該能看懂測試,乃至在必要時改動測試內容。

TDD:重點在於設計和生產力

測試驅動開發(TDD)是一種可持續交付優秀軟件的成熟技術。TDD的理念很簡單:在編寫生產代碼之前先編寫測試代碼。想要加一項功能?那就先寫一項測試吧。但要實踐好這種看似簡單的理念也需要技巧和判斷力。

TDD實際上是一種設計技能。TDD的本質是利用一系列小型測試來從零開始一步步設計系統,並在系統逐漸成型的過程中快速獲得價值。這種技能其實改名叫測試驅動設計更合適些。

作爲一種設計方法,TDD的重點在於專注和簡潔。它的目標是避免開發者編寫多餘的代碼,有些代碼是不產生交付價值的。TDD的理念是用最少的代碼來解決問題。

有很多文章稱讚TDD,列舉出了它的衆多優勢;很多技術會議上也會宣傳做測試的好處。這些宣傳並沒有誇大其辭,測試的確是非常必要的!TDD的優勢真實存在:

  • 它能讓你寫出更好的軟件。
  • 讓你避免多餘的勞動。
  • 當你引入新功能時,TDD能防止你搞砸一切。
  • 你的軟件因此能實現自我記錄。

雖說我一直很認可這些優勢,我也曾有一段時間覺得就算不用TDD我也能寫出優秀、可維護的軟件。當然現在我知道自己錯了,但爲什麼TDD好處這麼多我還會有那種念頭呢?是因爲成本!

TDD的成本太高了! 誠然,不做測試的話總成本的確會更高,但那種情況下成本會分攤在不同的階段。如果我們開始使用TDD就要立刻增加投入,相反,不用TDD的話代價在未來纔會體現出來。

順其自然完成工作是最理想的狀態。人的本性很懶惰(軟件開發者大概是懶惰的極致了)還貪婪,所以我們希望降低成本的手段能立刻生效——說得容易,做起來就難了!

圍繞TDD有許多理論、維度和視角,但我更想談一談TDD的實踐應用。最後,隨着我們一步步分析研究,我們會發現有些事情是隻有用TDD才能做到的。

這裏是我的演講:“單元測試和TDD理念與最佳實踐指南”,其中包含以下主題:

  • 爲什麼我們需要測試,
  • 測試類型,
  • 每種類型應該如何及何時使用,
  • 測試級別簡介,
  • 測試策略簡介,
  • TDD的實踐應用。

演講內容還包括指導和最佳實踐,介紹測試時的注意事項。

“TDD的實踐應用”這部分和演講中提到的理念是適用於所有編程語言的,而我演示時用的是Java。我們要展示的是設計和創作優秀作品時的思維過程,不是教人寫代碼那麼簡單。

分析問題

不管解決什麼問題,第一步就是不管問題有多複雜,先分析問題的結構並把它拆分成許多連續而完整的的解決步驟,同時考慮輸入場景和輸出的內容。接下來我們檢查這些步驟,從業務角度確保它們的結果和原始需求是一致的,不多也不少。這時候先不管具體的實現細節。

這是關鍵的一步;其中最重要的是要能識別出手頭上給定問題的所有需求,以減輕之後具體實現階段的負擔。將任務拆分成許多小步驟後,我們將獲得乾淨、易於實現、可測試的代碼。

TDD是開發和維護這些步驟的關鍵,我們要通過TDD來覆蓋手頭問題的所有可能情況。

假設我們需要開發一個轉換工具庫,功能是將羅馬數字轉換爲等效的阿拉伯數字。作爲開發者,我要做的事情有:

  1. 創建一個庫項目。
  2. 創建類。
  3. 應該會具體研究一下,開發出轉換的方法。
  4. 想一想可能存在的問題場景和應對方式。
  5. 爲這個任務編寫一個測試用例,先把寫測試的任務搞定(其實很可能寫不出來什麼東西),同時已經用老辦法幾乎做完測試工作了。

這種流程可太糟心了。

爲了在編寫代碼時正確啓動流程並將TDD付諸實踐,請遵照下面這些實踐步驟行事,這樣就能成功做好一個項目了;同時你還會得到一套測試用例,減輕未來開發工作的時間和成本負擔。

這個示例的代碼可以從我的GitHub庫克隆下來。啓動終端,指向自己喜歡的位置,然後運行以下命令:

$ git clone https://github.com/mohamed-taman/TDD.git

我已經做好了設置,項目的每次TTD紅色/綠色/藍色改動都會提交一次,因此在查看提交歷史時我們就能發現哪些改動和重構是符合最終項目需求的方向了。

我用的是Maven構建工具、Java SE 12和JUnit 5。

TDD的實踐應用

爲了開發上文所說的轉換器,我們要做的第一步是寫一個測試用例,將羅馬數字I轉換爲阿拉伯數字1。

這裏需要創建轉換器類和方法實現,以使測試用例滿足我們的第一個需求。

等等,等等!稍等一下!這裏有一條實踐中總結的經驗,最好記住這條規則再開始幹活兒:首先不要創建源代碼,而是先創建測試用例中的類和方法。這叫意圖編程,因爲我們在命名新類和將要使用新類的新方法時,必須要考慮我們正在編寫的這段代碼的用途和用法,這樣當然就會設計出更出色、更乾淨的API了。

第1步

首先創建一個測試包,還有類、方法和測試實現:

包:rs.com.tm.siriusxi.tdd.roman
類:RomanConverterTest
方法:convertI()
實現:assertEquals(1, new RomanConverter().convertRomanToArabicNumber(“I”));

第2步

這裏沒有失敗的測試用例:這是一個編譯錯誤。所以先用IDE提示在Java源文件夾中創建包、類和方法。

包:rs.com.tm.siriusxi.tdd.roman
類:RomanConverter
方法:public int convertRomanToArabicNumber(String roman)

第3步(紅色狀態)

我們需要確認我們指定的類和方法正確無誤,測試用例也能正常工作。

通過實現convertRomanToArabicNumber方法來拋出IllegalArgumentException,我們應該就能達到紅色狀態了。

public int convertRomanToArabicNumber(String roman) {
        throw new IllegalArgumentException();
 }

然後運行測試用例。我們應該能看到紅色指示條。

第4步(綠色狀態)

這一步我們需要再次運行測試用例,這次要看到綠色指示條。我們要用最少的代碼量滿足測試用例轉綠的要求。所以方法應該返回1這個值。

public int convertRomanToArabicNumber(String roman) {
        return 1;
 }

運行測試用例。我們應該能看到綠條了。

第5步(重構階段)

現在該做重構了(如果有能重構的代碼的話)。需要強調的是不僅生產代碼要重構,測試代碼也得重構。

從測試類中刪除未使用的導入。

再次運行測試用例,這次應該看到藍條——哈哈哈,開個玩笑,哪來的藍條。如果重構代碼後一切正常,我們應該能再次看到綠條。

刪除未使用的代碼是常用的簡單重構方法,可以提高代碼的可讀性,減少類的佔用空間,從而優化項目的存儲需求。

第6步

這一步開始,我們的流程就會統一成紅到綠到藍的順序。我們先寫一個問題的新需求或新步驟作爲失敗的測試用例來達成TDD的第一步,也就是紅色狀態,然後重複這一過程,直到我們完成整個功能。

注意,我們要從基本的需求或步驟起步,然後一步步走下去,直到我們完成所需的功能。這樣我們的路線就很清楚了,是由簡單到複雜的順序。

這樣的流程是最好的,不要花費大量時間從一開始就考慮整體實現,因爲這可能會讓我們想得太多,甚至搞出來一些不必要的功能。這樣做會導致過度編程,效率自然低下。

下一步工作是將羅馬數字II轉換爲阿拉伯數字2。

在同一個測試類中,我們創建了一個新的測試方法及其實現,如下所示:

方法:convertII()

當我們運行測試用例時會看到紅條,因爲convertII()方法測試失敗了。convertI()方法還是綠色狀態,這樣就很好,說明其它代碼沒有受新代碼影響。

第7步

現在我們需要讓測試用例跑出綠色狀態。我們來實現一種可以同時滿足兩種情況的方法。我們可以用簡單的if/else if/else檢查來處理這兩種情況,在else情況下我們會拋出IllegalArgumentException。

public int convertRomanToArabicNumber(String roman) {
        if (roman.equals("I")) {
            return 1;
        } else if (roman.equals("II")) {
            return 2;
        }
        throw new IllegalArgumentException();
    }

這裏要避免的一個問題是某行代碼(例如roman.equals(“I”))導致的空指針異常。要修復這個問題只需將相等情況轉換爲”I”.equals(roman)。

再次運行測試用例,所有情況都應該是綠條了。

第8步

現在我們可以設法重構案例了,聞一聞哪些代碼“味道不香”。重構時,我們通常會找出下面類型的代碼做調整:

  • 方法很長,
  • 重複代碼,
  • 很多if/else代碼,
  • switch-case語句,
  • 需要簡化的邏輯
  • 設計問題。

這個示例中的代碼問題(你找到了嗎)是if/else語句和返回太多。

也許我們應該引入一個sum變量,並使用for循環來遍歷羅馬字符的字符數組。如果一個字符是I,它將對sum加1,然後return變量sum。

但我喜歡防禦式編程,所以我會將throw子句移動到if語句的一個else語句中,以便覆蓋所有無效字符。

public int convertRomanToArabicNumber(String roman) {
        int sum = 0;
        for (char ch : roman.toCharArray()) {
            if (ch == 'I') {
                sum += 1;
            } else {
                throw new IllegalArgumentException();
            }
        }  
        return 0;
    }

在Java 10及更高版本中,我們可以使用var來定義變量,寫var sum = 0;代替int sum = 0;。

然後再次運行測試以確保我們的重構不會影響任何程序功能。

糟糕——我們看到了一個紅條。哦!原來所有測試用例都返回0,我們的錯誤是沒返回sum卻返回了0。

解決這個問題後就能看到美麗的綠條了。

這說明無論是多小的變動,都需要在添加改動後運行測試。重構時很容易引入錯誤,而測試用例就是我們的輔助工具;運行測試就能找出錯誤。這就是迴歸測試的力量。

再看一下代碼,還有另一種問題(你看到了嗎?)。這裏的異常不是描述性的,因此我們必須提供有意義的錯誤

message of Illegal roman character %s, ch.

throw new IllegalArgumentException(String.format("Illegal roman character %s", ch));

再次運行測試用例,所有情況都應該是綠條。

第9步

我們添加另一個測試用例,將羅馬數字III轉換爲3。

在同一個測試類中,我們創建一個新的測試方法及其實現:

方法:convertIII()

再次運行測試用例,全綠。我們的實現能覆蓋這種情況。

第10步

現在我們需要將羅馬數字V轉換爲5。

在同一個測試類中,我們創建一個新的測試方法及其實現:

方法:convertV()

運行測試用例會看到紅條,convertV()失敗了,其它部分還是綠色。

將else/if添加到主if語句來實現這個方法,並加入檢查,如果char = 'v’則sum+=5;。

for (char ch : roman.toCharArray()) {
            if (ch == 'I') {
                sum += 1;
            } else if (ch == 'V') {
                sum += 5;
            } else {
                throw new IllegalArgumentException(String.format("Illegal roman character %s", ch));
  } }

這裏我們可以重構,但不要在這一步做。在實現階段,我們唯一的目標是讓測試通過,看到一個綠條。現在我們不關心簡化設計、重構或代碼優化這些事情。當代碼通過測試後,我們可以回來再做重構。

在重構狀態下,我們只關心重構工作。一次只關注一件事以避免分心、提高效率。

測試用例應該是綠色狀態。

我們有時需要一串if/else語句;爲了優化它,可以按用例訪問頻率來對if語句測試用例排序。如果可以的話,改爲switch-case語句來跳過測試就更好了。

第11步

現在我們處於綠色狀態,輪到重構階段了。再來看方法,我們可以改動一些讓人討厭的if/else。

也許可以不用if/else,我們可以引入查詢表並將羅馬字符存儲爲鍵,將對應的阿拉伯數字存儲爲值。

那就刪除if語句並替換成sum += symbols.get(chr);。右擊氣泡,然後點擊引入實例變量。

private final Hashtable<Character, Integer> romanSymbols = new Hashtable<Character, Integer>() {
        {
            put('I', 1);
            put('V', 5);
        }
    };

我們需要像之前一樣檢查無效符號,因此我們讓代碼確定romanSymbols是否包含特定鍵,如果不包含就拋出異常。

public int convertRomanToArabicNumber(String roman) {
        int sum = 0;
        for (char ch : roman.toCharArray()) {
            if (romanSymbols.containsKey(ch)) {
                sum += romanSymbols.get(ch);
            } else {
                throw new IllegalArgumentException(
                        String.format("Illegal roman character %s", ch));
            }
        }
        return sum;
    }

運行測試用例,應該是綠色狀態。

這是另一種代碼問題,優化它是爲了更好的設計和性能,讓代碼更乾淨。最好使用HashMap而不是Hashtable,因爲與後者相比前者的實現是不同步的。對這種方法大量調用會損害性能。

一個設計思路是始終使用通用接口作爲目標類型,因爲這能帶來更容易維護,更乾淨的代碼,且在不影響代碼使用的情況下輕鬆調整實現細節。這個示例中我們將使用Map。

private static Map<Character, Integer> romanSymbols = new HashMap<Character, Integer>() {
        private static final long serialVersionUID = 1L;
        {
            put('I', 1);
            put('V', 5);
        }
    };

如果你用的是Java 9或更高版本,則可以使用新的HashMap<>()替換新的HashMap <Character,Integer>(),因爲菱形運算符可以與Java 9中的匿名內部類一起使用。

或者你可以使用更簡單的Map.of()。

Map<Character, Integer> romanSymbols = Map.of('I', 1, 'V', 5,'X', 10, 'L', 50,'C', 100, 'D', 500,'M', 1000);

java.util.Vector和java.util.Hashtable已過時。雖然它們仍然受支持,但這些類已被JDK 1.2集合類淘汰,新的應用不應該繼續用它們了。

重構之後我們需要檢查一切是否正常,確認沒有破壞任何東西。太棒了,是綠條!

第12步

這次找一些更有意思的數字做轉換。我們回到我們的測試類,實現將羅馬數字VI轉換爲6。

方法:convertVI()

我們運行測試用例,正常通過。看來我們編寫邏輯能自動覆蓋這種情況。這樣就不用單獨寫實現了。

第13步

現在我們需要將IV轉換爲4,這次可能就沒有VI轉成6那麼順利了。

方法:convertIV()

運行測試用例,果然出來的是紅條。

我們得設法讓它跑通了。衆所周知,在羅馬數字中較小數字的字符(例如I)如果附加到較大數字的字符(例如V)前面,相當於用大數減去小數——所以IV等於4,反過來VI等於6。

我們現有的代碼都是對數值求和運算的,但這一次我們需要做減法。我們應該做一個條件判定:如果前一個字符的值大於或等於後面字符的值,那麼就求和,反之就做減法。

先專心編寫滿足問題需要的邏輯,同時不考慮變量的聲明是很方便的做法。只要寫好邏輯,然後創建實現所需的變量就行了。如前所述,這就是意圖編程。這樣一來,我們就能更快地寫出最簡潔的代碼,不用事事都提前操心了——這纔是最棒的理念。

我們目前的實現是:

public int convertRomanToArabicNumber (String roman) {
        roman = roman.toUpperCase();
        int sum = 0;
        for (char chr : roman.toCharArray()) {
            if (romanSymbols.containsKey(chr))
                sum += romanSymbols.get(chr);
            else
                 throw new IllegalArgumentException(
                        String.format("Invalid roman character %s ", chr));
        }
        return sum;
    }

爲了檢查羅馬字符有效性,我們開始寫新的邏輯。先編寫一般性的邏輯,然後在IDE提示的幫助下創建一個局部變量。另外我們對變量的類型有一種感覺:它們要麼是方法的本地變量,要麼是實例/類變量。

int sum = 0, current = 0, previous = 0;
        for (char chr : roman.toCharArray()) {
            if (romanSymbols.containsKey(chr)) {
                if (previous >= current) {
                    sum += current;
                } else {
                    sum -= previous;
                    sum += (current - previous);
                }
            } else {
                throw new IllegalArgumentException(
                        String.format("Invalid roman character %s ", chr));
            }

現在我們需要將for循環更改爲基於索引以訪問當前和先前的變量,因此我們調整實現以滿足新改動的要求,以便正常編譯。

for (int index = 0; index < roman.length(); index++) {
            if (romanSymbols.containsKey(roman.charAt(index))) {
                  current = romanSymbols.get(roman.charAt(index));
                  previous = index == 0 ? 0 : romanSymbols.get(roman.charAt(index-1));
                if (previous >= current) {
                    sum += current;
                } else {
                    sum -= previous;
                    sum += (current - previous);
                }} else {
                throw new IllegalArgumentException(
                        String.format("Invalid roman character %s ", roman.charAt(index)));
            }

添加這個新功能後我們運行測試用例,綠條——很完美。

第14步

現在我們是綠色狀態,該做重構了。我們將嘗試做一個更有趣的重構。

我們的重構策略是儘量簡化代碼。觀察發現romanSymbols.get(roman.charAt(index))這行出現了兩次。

那就把重複的代碼提取到這裏要使用的方法或類中,讓將來的所有改動都集中在一起。

高亮代碼,右鍵單擊並選擇NetBeans重構工具>introduce>方法。將其命名爲getSymbolValue並保留爲私有方法,然後點“確定”。

現在需要運行測試用例,看看這個小型重構有沒有引入錯誤。結果代碼沒有出問題,我們發現它仍處於綠色狀態。

第15步

我們將做更多重構。聲明條件romanSymbols.containsKey(roman.charAt(index))很難閱讀,並且很難搞清它應該測試什麼來傳遞if語句。那就簡化代碼,使其更具可讀性。

雖然我們現在理解這行代碼的作用,但我保證半年後我們就很難理解它要做什麼了。

可讀性是我們應該通過TDD不斷提高的一項代碼質量關鍵指標,因爲在敏捷環境中我們經常要快速改動代碼——爲此代碼必須有很好的可讀性。另外任何改動都應該是可測試的。

現在把這行代碼提取到一個方法中,該方法的名稱爲doesSymbolsContainsRomanCharacter,名字就描述了它的作用。我們還是用NetBeans重構工具完成這項工作。

這樣做可以改善代碼的可讀性。這裏的條件是:如果符號包含羅馬字符,則執行邏輯,否則拋出無效字符的非法參數異常。

我們再次重新運行所有測試,沒出現新的錯誤。

請注意,不管引入多小的重構改動,都要跑一遍測試。我不會等做完所有的重構之後才運行所有測試用例。這在TDD中是非常重要的。我們需要即時反饋,運行測試用例就是我們的反饋循環。它讓我們儘早找出每個小步驟的錯誤,而不是對着一大串步驟的出錯信息發呆。

因爲每個重構步驟都可能引入新的錯誤,所以代碼改動和代碼測試之間的間隔時間越短,我們就能越快地分析代碼並修復新錯誤。

如果我們重構了一百行代碼然後運行測試用例結果不成功,就必須花時間調試以準確檢測出問題所在。在一百行代碼中找錯誤要比五行或十行代碼困難得多。

第16步

我們在引入的兩個私有方法和異常消息中有重複的代碼,重複的這行是roman.charAt(index),因此我們使用NetBeans將其重構爲一個新方法,名爲getCharValue(String roman, int index)。

重新運行所有測試,全部綠條。

第17步

現在做一些重構來優化代碼並提高性能。我們可以簡化轉換方法的計算邏輯。目前的邏輯是:

int convertRomanToArabicNumber(String roman) {
        roman = roman.toUpperCase();
        int sum = 0, current = 0, previous = 0;
        for (int index = 0; index < roman.length(); index++) {
            if (doesSymbolsContainsRomanCharacter(roman, index)) {
                current = getSymboleValue(roman, index);
                previous = index == 0 ? 0 : getSymboleValue(roman, index - 1);
                if (previous >= current) {
                    sum += current;
                } else {
                    sum -= previous;
                    sum += (current - previous);
                }
            } else {
                throw new IllegalArgumentException(
                        String.format("Invalid roman character %s ",
                                getCharValue(roman, index)));
            }
        }
        return sum;
    }

改進下面這一行可以節省幾個多餘的CPU週期:

previous = index == 0 ? 0 : getSymboleValue(roman, index - 1);

不必獲取前一個字符,因爲前一個字符只是for循環結束時的當前字符。可以刪除這一行並將其替換爲previous = current;,放在else條件末尾的計算後面。

運行測試用例,結果應該是綠色。

現在我們簡化計算以節省另外幾個多餘的計算週期。我會還原if語句測試用例的計算,並反轉for循環。最終的代碼應該是:

for (int index = roman.length() - 1; index >= 0; index--) {
            if (doesSymbolsContainsRomanCharacter(roman, index)) {
                current = getSymboleValue(roman, index);
                 if (current < previous) {
                    sum -= current;
                } else {
                    sum += current;
                }
                previous = current;
            } else {
                throw new IllegalArgumentException(
                        String.format("Invalid roman character %s ",
                                getCharValue(roman, index)));
            }

運行測試用例,應該還是綠色。

由於該方法不會更改任何對象狀態,因此可以將其設置爲靜態方法。此外該類是一個工具類,因此應該關閉它以便繼承。雖然所有方法都是靜態的,但我們不應該允許實例化類。添加一個私有默認構造函數來修復此問題,並將該類標記爲final。

現在我們會在測試類中出現編譯錯誤。一旦解決了這個問題,運行測試用例應該會再次全綠。

第18步

最後一步是添加更多測試用例,以確保我們的代碼涵蓋所有需求。

  1. 添加一個convertX()測試用例,它應該返回10,因爲羅馬數字X=10。這時運行測試會失敗並返回IllegalArgumentException,所以將X=10添加到符號映射裏。再次運行測試就通過了。這裏沒有重構內容。
  2. 添加convertIX()測試用例,它應該返回9,因爲IX=9。測試應該會通過。
  3. 在符號映射中加入這些值:L = 50,C = 100,D = 500,M = 1000。
  4. 添加一個convertXXXVI()測試用例,它應該返回36,因爲XXXVI=36。運行測試會正常通過。這裏沒有重構。
  5. 添加一個convertMMXII()測試用例,它應該返回2012。運行測試將通過。這裏沒有重構。
  6. 添加convertMCMXCVI()測試用例,它應該返回1996。運行測試將通過。這裏沒有重構。
  7. 添加一個convertInvalidRomanValue()測試用例,它應該拋出IllegalArgumentException。運行測試將通過。這裏沒有重構。
  8. 添加一個convertVII()測試用例,它應該返回7,因爲VII=7。但我們用小寫的vii測試時,測試將失敗並拋出IllegalArgumentException,因爲我們只處理了大寫字母。爲了解決這個問題,我們在方法開頭添加一行roman = roman.toUpperCase();。再次運行測試用例將通過。這裏沒有重構。

到這一步我們已經完成了任務(實現)。基於TDD的理念,我們用最少的代碼改動通過了所有測試用例,並通過重構滿足了所有需求,從而確保我們具有出色的代碼質量(性能、可讀性和設計)。

我希望大家像我一樣享受這個過程,希望這能鼓勵你在下一個項目甚至現在做的任務中開始使用TDD。喜歡本文的話請點擊分享、喜歡,並在GitHub中點星來幫我傳播吧。

作者介紹

Mohamed Taman是Comtrade數字服務公司的高級企業架構師、Java冠軍、甲骨文開拓大使。他是JCP成員,曾是JCP執行委員會成員,JSR 354、363、373專家組成員、EGJUG領導者、甲骨文埃及建築師俱樂部董事會成員。他主講Java,熱愛移動、大數據、雲、區塊鏈、DevOps。他是國際演講者,書籍和視頻“JavaFX essentials”“清潔代碼入門——Java SE 9”“動手實踐Java 10編程與JShell”的作者,還出了一本新書“Java冠軍的祕密”。他贏得2014、2015年杜克選擇獎項和JCP傑出參與者2013年獎項。

查看英文原文Test-Driven Development: Really, It’s a Design Technique

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章