測(cè)試驅(qū)動(dòng)開(kāi)發(fā)(TDD)

測(cè)試驅(qū)動(dòng)開(kāi)發(fā)(TDD)是一種軟件開(kāi)發(fā)過(guò)程,它依賴于非常短的開(kāi)發(fā)周期的重復(fù):首先,開(kāi)發(fā)人員編寫(xiě)一個(gè)(最初失敗的)自動(dòng)化測(cè)試用例,該用例定義了所需的改進(jìn)或新功能,然后產(chǎn)生最小量代碼以通過(guò)該測(cè)試,并最終將新代碼重構(gòu)為可接受的標(biāo)準(zhǔn)。

通常遵循以下步驟順序:

  • 添加測(cè)試
  • 運(yùn)行所有測(cè)試,看看新測(cè)試是否失敗
  • 寫(xiě)一些代碼
  • 運(yùn)行測(cè)試
  • 重構(gòu)代碼
  • 重復(fù)

在TDD上有很多文章,而Wikipedia始終是一個(gè)好的開(kāi)始。本文將重點(diǎn)介紹使用Roy Osherove Katas之一的變體進(jìn)行的實(shí)際測(cè)試和實(shí)現(xiàn)。

在下面,您將找到與每個(gè)需求以及隨后的實(shí)際實(shí)現(xiàn)相關(guān)的測(cè)試代碼。嘗試僅閱讀一個(gè)需求,自己編寫(xiě)測(cè)試和實(shí)現(xiàn),并將其與本文的結(jié)果進(jìn)行比較。請(qǐng)記住,有許多不同的方式來(lái)編寫(xiě)測(cè)試和實(shí)現(xiàn)。本文只是眾多可能的解決方案之一。

開(kāi)始吧!

要求

  • 使用int Add(string numbers)方法創(chuàng)建一個(gè)簡(jiǎn)單的String計(jì)算器
  • 該方法可以接受0、1或2個(gè)數(shù)字,并將返回它們的總和(對(duì)于空字符串,它將返回0),例如“”,“ 1”或“ 1,2”
  • 允許Add方法處理未知數(shù)量的數(shù)字
  • 允許Add方法處理數(shù)字之間的新行(而不是逗號(hào))。
  • 可以輸入以下內(nèi)容:“ 1 \ n2,3”(等于6)
  • 支持不同的定界符
  • 要更改定界符,字符串的開(kāi)頭將包含一個(gè)單獨(dú)的行,如下所示:“ // [delimiter] \ n [numbers…]”,例如“ //; \ n1; 2”應(yīng)返回三個(gè)默認(rèn)值分隔符為“;” 。
  • 第一行是可選的。所有現(xiàn)有方案仍應(yīng)受到支持
  • 用負(fù)數(shù)調(diào)用Add將引發(fā)異?!安辉试S使用負(fù)數(shù)”以及傳遞的負(fù)數(shù)。如果存在多個(gè)否定詞,請(qǐng)?jiān)谒挟惓O⒅酗@示所有否定詞(如果您是初學(xué)者,請(qǐng)?jiān)诖颂幫V梗?/li>
  • 大于1000的數(shù)字應(yīng)忽略,因此加2 + 1001 = 2
  • 分隔符可以是任何長(zhǎng)度,格式如下:“ // [delimiter] \ n”:“ // [-] \ n1-2-3”應(yīng)返回6
  • 允許這樣的多個(gè)定界符:“ // [delim1] [delim2] \ n”,例如“ // [-] [%] \ n1-2%3”應(yīng)返回6。
  • 確保您還可以處理長(zhǎng)度超過(guò)一個(gè)字符的多個(gè)定界符

即使這是一個(gè)非常簡(jiǎn)單的程序,僅查看那些要求也可能會(huì)令人不知所措。讓我們采取另一種方法。忘記您剛剛閱讀的內(nèi)容,讓我們一一滿足您的要求。

創(chuàng)建一個(gè)簡(jiǎn)單的字符串計(jì)算器

要求1:該方法可以采用0、1或2個(gè)數(shù)字,并用逗號(hào)(,)分隔。

讓我們編寫(xiě)第一組測(cè)試。

JAVA測(cè)試

package com.wordpress.technologyconversations.tddtest;
 
import org.junit.Test;
import com.wordpress.technologyconversations.tdd.StringCalculator;
 
public class StringCalculatorTest {
    @Test(expected = RuntimeException.class)
    public final void whenMoreThan2NumbersAreUsedThenExceptionIsThrown() {
        StringCalculator.add("1,2,3");
    }
    @Test
    public final void when2NumbersAreUsedThenNoExceptionIsThrown() {
        StringCalculator.add("1,2");
        Assert.assertTrue(true);
    }
    @Test(expected = RuntimeException.class)
    public final void whenNonNumberIsUsedThenExceptionIsThrown() {
        StringCalculator.add("1,X");
    }
}

最好以易于理解所測(cè)試內(nèi)容的方式來(lái)命名測(cè)試方法。我更喜歡使用[操作]然后[驗(yàn)證]的BDD變體。在這種情況下,一種測(cè)試方法的名稱(chēng)是whenMoreThan2NumbersAreUsedThenExceptionIsThrown。我們的第一組測(cè)試驗(yàn)證了最多兩個(gè)數(shù)字可以傳遞給計(jì)算器的add方法。如果有兩個(gè)以上,或者其中一個(gè)不是數(shù)字,則應(yīng)引發(fā)異常。在@Test批注中放入“ expected”會(huì)告訴JUnit運(yùn)行器預(yù)期的結(jié)果是拋出指定的異常。為了簡(jiǎn)潔起見(jiàn),從此處開(kāi)始,將僅顯示代碼的修改部分??梢詫⒄麄€(gè)代碼分為需求,可以從GitHub存儲(chǔ)庫(kù)中獲?。y(cè)試和實(shí)施)。

JAVA實(shí)現(xiàn)

public class StringCalculator {
    public static final void add(final String numbers) {
        String[] numbersArray = numbers.split(",");
        if (numbersArray.length > 2) {
            throw new RuntimeException("Up to 2 numbers separated by comma (,) are allowed");
        } else {
            for (String number : numbersArray) {
                Integer.parseInt(number); // If it is not a number, parseInt will throw an exception
            }
        }
    }
}

請(qǐng)記住,TDD背后的想法是做必要的最少工作,以使測(cè)試通過(guò)并重復(fù)該過(guò)程,直到實(shí)現(xiàn)整個(gè)功能為止。目前,我們僅對(duì)確?!霸摲椒梢越邮?、1或2個(gè)數(shù)字”。再次運(yùn)行所有測(cè)試,然后查看它們是否通過(guò)。

要求2:對(duì)于空字符串,該方法將返回0

JAVA測(cè)試

@Test
public final void whenEmptyStringIsUsedThenReturnValueIs0() {
    Assert.assertEquals(0, StringCalculator.add(""));
}

JAVA實(shí)現(xiàn)

public static final int add(final String numbers) { // Changed void to int
    String[] numbersArray = numbers.split(",");
    if (numbersArray.length > 2) {
        throw new RuntimeException("Up to 2 numbers separated by comma (,) are allowed");
    } else {
        for (String number : numbersArray) {
            if (!number.isEmpty()) {
                Integer.parseInt(number);
            }
        }
    }
    return 0; // Added return
}

要使此測(cè)試通過(guò),要做的就是將return方法從void更改為int并以返回0結(jié)束。

要求3:方法將返回它們的數(shù)字總和

JAVA測(cè)試

@Test
public final void whenOneNumberIsUsedThenReturnValueIsThatSameNumber() {
    Assert.assertEquals(3, StringCalculator.add("3"));
}
 
@Test
public final void whenTwoNumbersAreUsedThenReturnValueIsTheirSum() {
    Assert.assertEquals(3+6, StringCalculator.add("3,6"));
}

JAVA實(shí)現(xiàn)

public static int add(final String numbers) {
    int returnValue = 0;
    String[] numbersArray = numbers.split(",");
    if (numbersArray.length > 2) {
        throw new RuntimeException("Up to 2 numbers separated by comma (,) are allowed");
    }
    for (String number : numbersArray) {
        if (!number.trim().isEmpty()) { // After refactoring
            returnValue += Integer.parseInt(number);
        }
    }
    return returnValue;
}

在這里,我們?cè)谒袛?shù)字中添加了迭代以創(chuàng)建總和。

要求4:允許Add方法處理未知數(shù)量的數(shù)字

JAVA測(cè)試

//  @Test(expected = RuntimeException.class)
//  public final void whenMoreThan2NumbersAreUsedThenExceptionIsThrown() {
//      StringCalculator.add("1,2,3");
//  }
    @Test
    public final void whenAnyNumberOfNumbersIsUsedThenReturnValuesAreTheirSums() {
        Assert.assertEquals(3+6+15+18+46+33, StringCalculator.add("3,6,15,18,46,33"));
    }

JAVA實(shí)現(xiàn)

public static int add(final String numbers) {
    int returnValue = 0;
    String[] numbersArray = numbers.split(",");
    // Removed after exception
    // if (numbersArray.length > 2) {
    // throw new RuntimeException("Up to 2 numbers separated by comma (,) are allowed");
    // }
    for (String number : numbersArray) {
        if (!number.trim().isEmpty()) { // After refactoring
            returnValue += Integer.parseInt(number);
        }
    }
    return returnValue;
}

要實(shí)現(xiàn)此要求,我們要做的就是刪除部分代碼(如果有兩個(gè)以上的數(shù)字,則會(huì)引發(fā)異常)。但是,一旦執(zhí)行測(cè)試,第一個(gè)測(cè)試就會(huì)失敗。為了滿足此要求,需要?jiǎng)h除whenMoreThan2NumbersAreUsedThenExceptionIsThrown時(shí)的測(cè)試。

要求5:允許Add方法處理數(shù)字之間的新行(而不是逗號(hào))。

JAVA測(cè)試

@Test
public final void whenNewLineIsUsedBetweenNumbersThenReturnValuesAreTheirSums() {
    Assert.assertEquals(3+6+15, StringCalculator.add("3,6n15"));
}

JAVA實(shí)現(xiàn)

public static int add(final String numbers) {
    int returnValue = 0;
    String[] numbersArray = numbers.split(",|n"); // Added |n to the split regex
    for (String number : numbersArray) {
        if (!number.trim().isEmpty()) {
            returnValue += Integer.parseInt(number.trim());
        }
    }
    return returnValue;
}

我們要做的就是通過(guò)添加| \ n來(lái)擴(kuò)展split regex。

要求6:支持不同的定界符

要更改定界符,字符串的開(kāi)頭將包含一個(gè)單獨(dú)的行,如下所示:“ // [delimiter] \ n [numbers…]”,例如“ //; \ n1; 2”應(yīng)采用1和2作為參數(shù)并返回3,默認(rèn)分隔符為';' 。

JAVA測(cè)試

@Test
public final void whenDelimiterIsSpecifiedThenItIsUsedToSeparateNumbers() {
    Assert.assertEquals(3+6+15, StringCalculator.add("http://;n3;6;15"));
}

JAVA實(shí)現(xiàn)

public static int add(final String numbers) {
    String delimiter = ",|n";
    String numbersWithoutDelimiter = numbers;
    if (numbers.startsWith("http://")) {
        int delimiterIndex = numbers.indexOf("http://") + 2;
        delimiter = numbers.substring(delimiterIndex, delimiterIndex + 1);
        numbersWithoutDelimiter = numbers.substring(numbers.indexOf("n") + 1);
    }
    return add(numbersWithoutDelimiter, delimiter);
}
 
private static int add(final String numbers, final String delimiter) {
    int returnValue = 0;
    String[] numbersArray = numbers.split(delimiter);
    for (String number : numbersArray) {
        if (!number.trim().isEmpty()) {
            returnValue += Integer.parseInt(number.trim());
        }
    }
    return returnValue;
}

這次有很多重構(gòu)。我們將代碼分為2種方法。初始方法解析輸入以查找定界符,隨后調(diào)用執(zhí)行實(shí)際總和的新輸入。由于我們已經(jīng)具有涵蓋所有現(xiàn)有功能的測(cè)試,因此可以安全地進(jìn)行重構(gòu)。如果有任何問(wèn)題,則其中一項(xiàng)測(cè)試將發(fā)現(xiàn)問(wèn)題。

要求7:負(fù)數(shù)將引發(fā)異常

用負(fù)數(shù)調(diào)用Add將引發(fā)異?!安辉试S使用負(fù)數(shù)”以及傳遞的負(fù)數(shù)。如果存在多個(gè)否定詞,請(qǐng)?jiān)诋惓O⒅酗@示所有否定詞。

JAVA測(cè)試

@Test(expected = RuntimeException.class)
public final void whenNegativeNumberIsUsedThenRuntimeExceptionIsThrown() {
    StringCalculator.add("3,-6,15,18,46,33");
}
@Test
public final void whenNegativeNumbersAreUsedThenRuntimeExceptionIsThrown() {
    RuntimeException exception = null;
    try {
        StringCalculator.add("3,-6,15,-18,46,33");
    } catch (RuntimeException e) {
        exception = e;
    }
    Assert.assertNotNull(exception);
    Assert.assertEquals("Negatives not allowed: [-6, -18]", exception.getMessage());
}

有兩個(gè)新測(cè)試。第一個(gè)檢查是否存在負(fù)數(shù)時(shí)是否引發(fā)異常。第二個(gè)驗(yàn)證異常消息是否正確。

JAVA實(shí)現(xiàn)

private static int add(final String numbers, final String delimiter) {
    int returnValue = 0;
    String[] numbersArray = numbers.split(delimiter);
    List negativeNumbers = new ArrayList();
    for (String number : numbersArray) {
        if (!number.trim().isEmpty()) {
            int numberInt = Integer.parseInt(number.trim());
            if (numberInt < 0) {
                negativeNumbers.add(numberInt);
            }
            returnValue += numberInt;
        }
    }
    if (negativeNumbers.size() > 0) {
        throw new RuntimeException("Negatives not allowed: " + negativeNumbers.toString());
    }
    return returnValue;     
}

添加了此時(shí)間代碼,該代碼收集列表中的負(fù)數(shù),如果有則拋出異常。

要求8:大于1000的數(shù)字應(yīng)被忽略

示例:加2 + 1001 = 2

JAVA測(cè)試

@Test
public final void whenOneOrMoreNumbersAreGreaterThan1000IsUsedThenItIsNotIncludedInSum() {
    Assert.assertEquals(3+1000+6, StringCalculator8.add("3,1000,1001,6,1234"));
}

JAVA實(shí)現(xiàn)

private static int add(final String numbers, final String delimiter) {
        int returnValue = 0;
        String[] numbersArray = numbers.split(delimiter);
        List negativeNumbers = new ArrayList();
        for (String number : numbersArray) {
                if (!number.trim().isEmpty()) {
                        int numberInt = Integer.parseInt(number.trim());
                        if (numberInt < 0) {
                                negativeNumbers.add(numberInt);
                        } else if (numberInt <= 1000) {
                                returnValue += numberInt;
                        }
                }
        }
        if (negativeNumbers.size() > 0) {
                throw new RuntimeException("Negatives not allowed: " + negativeNumbers.toString());
        }
        return returnValue;                
}

這個(gè)很簡(jiǎn)單。我們移動(dòng)了“ returnValue + = numberInt;” 在“ else if(numberInt <= 1000)”中。

還有3個(gè)需求。我鼓勵(lì)您自己嘗試。

要求9:定界符可以是任何長(zhǎng)度

應(yīng)使用以下格式:“ // [定界符] \ n”。示例:“ // [—] \ n1—2-3”應(yīng)返回6

要求10:允許使用多個(gè)定界符

應(yīng)該使用以下格式:“ // [delim1] [delim2] \ n”。示例“ // [-] [%] \ n1-2%3”應(yīng)返回6。

要求11:確保您還可以處理長(zhǎng)度超過(guò)一個(gè)字符的多個(gè)定界符

給TDD一個(gè)機(jī)會(huì)

對(duì)于TDD初學(xué)者來(lái)說(shuō),整個(gè)過(guò)程通常看起來(lái)不堪重負(fù)。常見(jiàn)的抱怨之一是TDD拖慢了開(kāi)發(fā)過(guò)程。確實(shí),起初要花時(shí)間才能加快速度。但是,在使用TDD流程進(jìn)行一些實(shí)踐開(kāi)發(fā)之后,可以節(jié)省時(shí)間,進(jìn)行更好的設(shè)計(jì),可以輕松安全地進(jìn)行重構(gòu),提高質(zhì)量和測(cè)試覆蓋率,并且最后但并非最不重要的一點(diǎn)是確保始終對(duì)軟件進(jìn)行測(cè)試。TDD的另一個(gè)巨大好處是,測(cè)試可以作為活動(dòng)文檔。查看測(cè)試即可知道每個(gè)軟件單元應(yīng)該做什么。只要所有測(cè)試通過(guò),該文檔就始終是最新的。用TDD進(jìn)行的單元測(cè)試應(yīng)為大多數(shù)代碼提供“代碼覆蓋率”,并且應(yīng)與驗(yàn)收測(cè)試驅(qū)動(dòng)開(kāi)發(fā)(ATDD)或行為驅(qū)動(dòng)開(kāi)發(fā)(BDD)。它們一起涵蓋了單元測(cè)試和功能測(cè)試,并提供了完整的文檔和要求。

TDD使您專(zhuān)注于您的任務(wù),準(zhǔn)確地編寫(xiě)所需的代碼,從外部進(jìn)行思考,最終成為一個(gè)更好的程序員。

參考

Technology Conversations

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,345評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,494評(píng)論 3 416
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事?!?“怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 176,283評(píng)論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 62,953評(píng)論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,714評(píng)論 6 410
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 55,186評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,255評(píng)論 3 441
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 42,410評(píng)論 0 288
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,940評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,776評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,976評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,518評(píng)論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,210評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 34,642評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 35,878評(píng)論 1 286
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,654評(píng)論 3 391
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,958評(píng)論 2 373

推薦閱讀更多精彩內(nèi)容