TDD練習 Codewars_BoiledEggs

這份題目的需求是煮蛋要花多少時間,運算過後將需要的時間輸出。

Codewars kata題目: Boiled Eggs TDD練習。

我的Git Commit 紀錄 

這篇是第二篇關於TDD練習的文章,TDD練習的紀錄文章真的是不太好寫,這一次的題目感覺起來比較好寫一點就決定寫出來了 www

之前有預備了十幾道TDD練習的記錄在git上 左思右想 還是覺得 放著給自己看就好了(被揍

這題會PO出來的原因是,因為自己在做這題的時候發現我自己根本沒搞清楚題目的需求,且又看了codewars上的測試案例誤導了自己
不過也好家在前面的這些 Test Case 讓我知道修改過後的Code 是不是會像之前一樣保證正確

一樣是Codewars的題目,所以這一個題目的需求也十分單純,比上一次還要簡單,這一次的需求是

  • 蛋的數量要分幾次煮(需求是一次最多可以煮八顆蛋)
  • 分幾次煮的次數乘以煮一次要花多少的時間(需求是煮一次要花5分鐘)

首先要說的是蛋要分幾次煮,因為需求輸入限制 int 所以要將輸入轉型 -> ToDouble ,ToDouble之後為了運算求取其次數,所以需要Math.Ceiling

分幾次煮的次數則是在需求中提到是5分鐘,故將上述算出來的數值成以5即可

首先先初始化專案,產生第一個測試案例並讓他測試失敗

[TestClass]
    public class UnitTest1
    {
        [TestMethod]
        public void Input_0_Should_Be_0()
        {
            //arrange
            var input = 0;
            var expected = 0;
            //act
            var actual = Kata.CookingTime(input);
            //assert
            Assert.AreEqual(expected, actual);
        }
    }

以下為Production Code,它使得我們的第一個測試亮出了紅燈

public class Kata
    {
        public static int CookingTime(int input)
        {
            throw new NotImplementedException();
        }
    }

接著用最簡單的方式去讓他變成綠燈,其Production Code 如下

public class Kata
    {
        public static int CookingTime(int input)
        {
            return 0;
        }
    }

接下來寫出第二個測試案例,此測試案例目的是確保原本的code是否可以符合我們的需求(結果自己需求看錯QAQ)

public void Input_1_Should_Be_5()
{
    //arrange
    var input = 1;
    var expected = 5;
    //act
    var actual = Kata.CookingTime(input);
    //assert
    Assert.AreEqual(expected, actual);
}

將測試案例完成之後,其Production Code變成這樣(超級簡陋),這時候其實就已經偏離需求了,因為需求一開始沒看清楚以為是五顆一次...所以變成乘以五,應該要是八顆一次,如果有看對需求就不會這樣做了...

public class Kata
    {
        public static int CookingTime(int input)
        {
            return input * 5;
        }
    }

不過沒關係,我們先繼續看下去,目前的Test Code 就發現有重複的狀況,所以針對這一部分進行重構,將Code變成以下如此

[TestClass]
    public class UnitTest1
    {
        [TestMethod]
        public void Input_0_Should_Be_0()
        {
            cookingTimeResult(0,0);
        }

        [TestMethod]
        public void Input_1_Should_Be_5()
        {
            cookingTimeResult(5,1);
        }

        private static void cookingTimeResult(int expected,int input)
        {
            //act
            var actual = Kata.CookingTime(input);
            //assert
            Assert.AreEqual(expected, actual);
        }
    }

看起來舒服多了,對吧?
重構完成之後不要忘記記再 run 一次測試確保我們修改的 Code 有沒有出錯

接下來新增了第三個測試案例,接下來做測試案例都很方便,只需要寫一行就可以了,其測試的資料及 Code 如下

public void Input_2_Should_Be_5()
        {
            cookingTimeResult(5, 2);
        }

測試之後就會發現理所當然的錯了,所以就開始修改Production Code,因為需求到目前為止都看錯的問題,所以變成是以為需求是最多5顆一次,其Production Code就變成下面這個樣子,run了一遍測試,OK 過了 綠燈

public class Kata
    {
        public static int CookingTime(int input)
        {
            var result = 0;
            var ceilNum = Math.Ceiling(Convert.ToDouble(input) / 5);
            result = (int)ceilNum * 5;
            return result;
        }
    }

接下來要重構程式碼,因為上面這串實在是有點長,所以重構後就變成如此。

public class Kata
    {
        public static int CookingTime(int input)
        {
            return (int)Math.Ceiling(Convert.ToDouble(input) / 5) * 5;
        }
    }

也跟著加上了一些測試案例,而這些案例,都是根據錯誤的需求理解來製造的,所以表面上過了,在提交之後就發生了錯誤QQ,新增的測試案例如下,從輸入11應該要是15就是一個很血淋淋的例子

[TestMethod]
public void Input_5_Should_Be_5()
{
    cookingTimeResult(5,5);
}

[TestMethod]
public void Input_10_Should_Be_10()
{
    cookingTimeResult(10,10);
}

[TestMethod]
public void Input_11_Should_Be_15()
{
    cookingTimeResult(15,11);
}

提交之後終於發現需求理解錯誤,就將程式更改了,其Production Code變成以下如此,並且Run一次測試,卻出現了錯誤,因為剛寫的測試案例本身就是一個錯誤,輸入11應該要是10,所以出現了紅燈

public class Kata
    {
        public static int CookingTime(int input)
        {
            return (int)Math.Ceiling(Convert.ToDouble(input) / 8) * 5;
        }
    }

接著就將那一道測試案例更改成輸入11應該要輸出10,更改完成之後測試就過了

public void Input_11_Should_Be_10()
{
    cookingTimeResult(10,11);
}

接著就加入最關鍵的測試案例,輸入8,應該要是5,其Code如下

public void Input_8_Should_5()
{
    cookingTimeResult(5, 8);
}

測試過了之後就將 Code 提交至 Codewars 上了,最後的 Production Code 和所有測試案例,如下

[TestClass]
    public class UnitTest1
    {
        [TestMethod]
        public void Input_0_Should_Be_0()
        {
            cookingTimeResult(0, 0);
        }

        [TestMethod]
        public void Input_1_Should_Be_5()
        {
            cookingTimeResult(5, 1);
        }

        [TestMethod]
        public void Input_2_Should_Be_5()
        {
            cookingTimeResult(5, 2);
        }

        [TestMethod]
        public void Input_5_Should_Be_5()
        {
            cookingTimeResult(5, 5);
        }

        [TestMethod]
        public void Input_10_Should_Be_10()
        {
            cookingTimeResult(10, 10);
        }

        [TestMethod]
        public void Input_11_Should_Be_10()
        {
            cookingTimeResult(10, 11);
        }

        [TestMethod]
        public void Input_8_Should_5()
        {
            cookingTimeResult(5, 8);
        }

        private static void cookingTimeResult(int expected, int input)
        {
            //act
            var actual = Kata.CookingTime(input);
            //assert
            Assert.AreEqual(expected, actual);
        }
    }

    public class Kata
    {
        public static int CookingTime(int eggs)
        {
            var minutes = 5;
            return CookTimes(eggs) * minutes;
        }

        public static int CookTimes(int eggs)
        {
            var eggsPerPot = 8;
            return (int) Math.Ceiling(Convert.ToDouble(eggs) / eggsPerPot);
        }
    }

原以為自己的Code一樣還是覺得很簡短了,結果有人用了Lamda的方式完成他,也有用更好的方式完成他,他們的方式就如下幾張圖,我又覺得很興奮自己還有很多東西是可以學的了 A____A

第一個則是剛才說的Lamda方式,第二個是用遞迴的方式完成,第三個是用三元運算式來做
這三個看起來都很厲害啊!!! 比我的厲害的感覺 XDDDD

提交完成之後為了讓Code更好維護和彈性,就將程式碼改成了下面這個樣子

public class Kata
    {
        public static int CookingTime(int eggs)
        {
            var minutes = 5;
            return CookTimes(eggs) * minutes;
        }

        public static int CookTimes(int eggs)
        {
            var eggsPerPot = 8;
            return (int) Math.Ceiling(Convert.ToDouble(eggs) / eggsPerPot);
        }
    }

之後如果廚房的鍋子變大了 可以裝更多的蛋,就可以更改CookTimes(煮的次數)方法裡的 eggsPerPot 這個變數,一目了然
minutes這個變數也是比較讓人好閱讀一點就不會滿臉問號的覺得5是什麼意思
不知道有沒有大大可以提出更好看的方式? 歡迎一起討論 <(_ _)>

以上是我這一次的TDD練習,這一次的心得就是

需求真的要看清楚

不然真的搞到最後好一點就像這個題目一樣,要修改的地方很小,壞的話就真的可能牽扯到各種事項,很有可能要把所有的Code做修改,連測試案例預期的值都會變成是錯誤的

不過在也因為今天這個情況更讓自己覺得TDD的開發更有意義,因為我在修改Code之後可以用之前撰寫完成的測試案例立即的驗證自己改的是不是對的

再強調一次,給自己一個教訓QAQ

需求真的要看清楚

需求真的要看清楚

需求真的要看清楚

不然真的....會很慘 O_O|||

這裡是本文的commit history


91在介紹codewars以及跟我私聊幾句時說了下面這段話

我們需要找到一種持續給自己回饋的方式,才能持續改善
所以 scrum 裡面有 iteration (迭代)
所以有 retrospective
一切都是持續改善的基本要素

我想Codewars就是一個很棒的回饋方式,因為你以為你已經寫到很精簡的Code時,在提交之後就會發現一個新的世界!!!