一步一腳印學習TDD

91親自指導的TDD的心得文

昨天晚上有幸讓91大神親自指導TDD,趕快趁還沒忘光前把學到的東西記錄下來,

學習TDD的練習題是codewars上的練習題 : mumbling

再開始動手解題前,先要建立codewars帳號(廢話

codewars創帳號的驗證很有趣,他要通過驗證的方式是要你修改一段code錯誤的地方,要改對才能通過pass

登入codewars後,我們趕快來看一下題目吧~

題目的examples

    1. input : abcd => output : A-Bb-Ccc-Dddd
    2. input :RqaEzty => output : R-Qq-Aaa-Eeee-Zzzzz-Tttttt-Yyyyyy

題目的說明就是examples,其實就可以看出需求和想到時做的,那我們就來開始動手做吧

從這開始我會試著用step by step的方式寫,希望能夠把TDD的流程以及我在跑TDD時的發現與體會完整的敘述出來

step 1. 打開Visual Studio 建立 project

    選擇C#->Test->Unit Test Project,

step 2. 設定source control

    這步驟可以忽略因為我也沒建成功XD~nuget上找不到github的套件

step 3. 建立第一個測試案例

        [TestMethod]
        public void Input_a_shouldReturn_A()
        {
               var sut = new Mumbling();
            string actual = sut.Accum("a");

            Assert.AreEqual("A", actual);
        }
        
        Mumbling Class和 Accum function都是用熱建記得,但是現在卻是不出來惹....
        
 step 4. Run Unit Test(Ctrl+R+A)亮第一個紅燈
 
        完成測試代碼後,別急著想要實作功能,先run unit test,這次會亮第一個紅燈
        
step 5. 完成第一項測試的實作代碼
 
                 亮紅燈後在開始針對測試實作功能
          internal class Mumbling
          {
              internal string Accum(string input)
              {

                return input.ToUpper();  
                
              }
          }
          
step 6. Run Unit Test亮第一個綠燈
              
           完成實作後再run unit test,讓紅燈轉綠燈,這樣我們就完成第一輪惹
        
step 7 第二個測試案例
  
              [TestMethod]
        public void Input_ab_shouldReturn_A_dash_Bb()
        {
               var sut = new Mumbling();
            string actual = sut.Accum("ab");

            Assert.AreEqual("A-Bb", actual);
        }
    
step 8 refator
    
        完成第二個測試案例後就會發現有東西可以重構惹,我們可以很明顯的發現
              var sut = new Mumbling();
            string actual = sut.Accum("ab");

            Assert.AreEqual("A-Bb", actual);
        上面都是重複的部分我們可以提取出來做成新的metod
        請按Ctrl+R,M  
        再跳出的視窗中,name填AccumShouldBe
        太神奇惹捷克~!!這樣就建起了AccumShouldBe method並把code提取過來
        現在code會長這樣
            [TestMethod]
          public void Input_a_shouldReturn_A()
          {
              AccumShouldBe("A", "a");
          }

          [TestMethod]
          public void Input_ab_shouldReturn_A_dash_Bb()
          {
              AccumShouldBe("A-Bb", "ab");
          }

          private static void AccumShouldBe(string expected, string input)
          {
              var sut = new Mumbling();
              var actual = sut.Accum(input);

              Assert.AreEqual(expected, actual);
          }
          
step 9 Run Unit Test亮第二個紅燈
     
             一樣在實作功能前先把紅燈亮起來
      
step 10 完成第二個測試案例的實作代碼
      
          internal string Accum(string input)
        {
            var count = 0;
                        string output = string.Empty
            foreach (char @char in input)
            {
                string upperChar = @char.ToString().ToUpper();
                output = output + upperChar;
                
                for (int i = 0; i < count; i++)
                {
                    output.Add(lowerChar);
                }

                count++;

                                output = output + "-";
            }

            return output.Substring(0,output.Length-1);                
            
        }
       
step 11 Run Unit Test亮第二個綠燈
    
step 12 思考一下
    
    完成這兩個測試案例後,我回頭去看我完成的功能實作,
    乍看之下感覺功能好像完成了,但是心裡感覺還是有點虛虛的,
    又思考了一下後想到好像沒考慮到Input空字串的情況,
    所以寫了第三個測試案例
    
step 13第三個測試案例
    
          [TestMethod]
        public void InputIsEmpty_shouldReturnEmpty()
        {
            AccumShouldBe("", "");
        }
    
step 14 Run Unit Test
    
        亮紅燈!?看一下錯誤代碼發現當string是空字串使用subString會產生exception
      
step 15 修正紅燈
            
      在修改錯誤時,我很直覺得把output.Substring(0,output.Length-1)的部分,包了一層if判斷output是否為空字串
      if(output != string.Empty){
        return output.Substring(0,output.Length-1);                
      }else{
          return string.Empty;
      }
      
      91看到當下立刻點出我的問題: 判斷空字串的邏輯應該放在開頭而且是判斷Input是否為空值
      問題發生的原因是因Input為空值才跳出這錯誤(91有提到一個詞,小弟忘記叫什麼惹..好像叫關注點錯誤..
      這東西我認為在解決問題的能力上是很重要的部分,也是我需要加強的部分
      
      另外還提到判斷string null or empty要使用string.IsNullOrEmpty()
         
      所以在前面加了
      if (string.IsNullOrEmpty(input))
      {
          return string.Empty;
      }
        並把output判斷那段拿掉
      
step 15 run unit test
        綠燈~~
      
           通過測試後,我又很下意識的回頭去check 實作代碼
      來來回回看起次,心理覺得妥妥的應該是沒什麼問題了
      91要我再測一個 input是 AB的測試案例
      
step 16 測試案例InputIsAB_shouldReturn_A_dash_Bb
        [TestMethod]
        public void InputIsAB_shouldReturn_A_dash_Bb()
        {
            AccumShouldBe("A-Bb", "AB");
        }
step 17 run unit test
            哪尼!又亮紅燈!?
        test fail吐出來的message,output是A-BB
        原來沒有處理到第二個repeate的字要小寫的需求
        
        這時才深刻的體悟到TDD的重要性
        
        從到目前的TDD過程中我體悟到:
        沒有完整的測試案例,將無法確保程式的正確性,更別說沒有經過unit test所產出的程式了,
        而想要撰寫出完整的測試案例,必須通過不斷練習TDD,
        在為每段程式撰寫測試案例的過程中,鍛鍊出如何設計完整的測試案例
        
step 18 finail code
         internal string Accum(string input)
        {

            if (string.IsNullOrEmpty(input))
            {
                return string.Empty;
            }

            var output = string.Empty;
            var count = 0;

            foreach (char aChar in input)
            {
                string upperChar = aChar.ToString().ToUpper();
                output = output + upperChar;
                
                for (int i = 0; i < count; i++)
                {
                    string lowerChar = upperChar.ToLower();
                    output = output + lowerChar;
                }

                count++;

                output = output + "-";
            }

            return output.Substring(0, output.Length - 1);                
            
        }
     
step 19 best solution
             都亮綠燈後把code貼上codewars上sumbit後就過關了,看了best solution盡然只要一行,
        91說一行不一定是最好的,並給我看他的solution
        public static String Accum(string s)
        {
            var result = s.Select((c, i) => ToUpperChar(c) + GetRepeatLetters(c, i));
            return string.Join("-", result);
        }

        private static string GetRepeatLetters(char letter, int repeatTimes)
        {
            return string.Concat(Enumerable.Repeat(letter.ToString().ToLower(), repeatTimes));
        }

        private static string ToUpperChar(char letter)
        {
            return letter.ToString().ToUpper();
        }
        
        我也是比較喜歡91的版本,我這樣說真的不是為了要吹捧他(這幾天我已經吹到他會怕惹XD
        而是因為我也認同程式的可讀性比你程式寫得多潮多屌來得有意義多惹
        
結論:
    - 對重構又有更深刻的體驗,感覺到重構後程式碼更短了更直覺了,讀code的思考時間變短了,寫code速度也更快了
    - 能跟91 pair programing真的是小弟三生有幸R...,一次可抵十年功。
          但同時又遇到自己的老問題,聽得懂學得會但是記不下來,還沒來得及消化之前就先忘了大半了,
        這也是為什麼我想寫這篇文章,紀錄這些珍貴的知識,
        試著用寫文章的方式將學到的東西內化,回憶忘掉的那部分。
        
        最近換了新工作後,自己以往的缺點又暴露了出來,
        在這裡也勉勵自己一下:
        每個人都會有自己的缺點,與其為了自己的缺點沮喪,不如試著思考如何一點一點的改善它,不會講話,
        就思考如何可以講得更好,讓每次表達都能夠比前一次更好
    - Code Snippets是個好東西,可以讓你很快的打出重構後的method名稱
    - 過程中91都不准我用滑鼠,強迫我用熱建,其實按了幾次下來有幾個熱鍵就按得很熟了,
      這也讓我體會到想要記住一樣東西,不斷的練習是一個非常有效的方法

以上是我這次練習TDD所學到以及體會到的東西 

其實之後還會再繼續重構它

所以這篇應該算未完待續(第一次寫blog...盡然寫惹3葛多小時0.0
        
-----------------------------------------------------------------------------------------------    

重構篇

最近實在太忙惹...,學的東西太多搞到自己暈頭轉向的,拖稿了這麼多天,不囉嗦趕快開始吧~!

重構第一版

    1. 使用string.Join取代原本串起"-"的方式
        return string.Join("-", ToUpperFirstThenToLowerAndRepeat(input));
      ToUpperFirstThenToLowerAndRepeat是回傳List<string>
    
    2. 將第二層foreach提取成method取名toLowerAndRepeatChar
    
    3. 在toLowerAndRepeatChar裡在將RepeatChar功能提取出來

心得: 這次重構的部分主要是真段程式碼提取成方法,讓業務邏輯在是整陀攤在main裡面,主要的目的是增加可讀性,
         當然string,join也是我以前沒用過的東東,雖然常看到,但是還是要真正用過一次才是知道他到底如何使用,
     重構完以後我仍然認為有許多地方是可以改進的,尤其是針對方法的命名上,所以我很不要臉地請教91幫我review code,
     於是有惹加強版

重構加強版
91看了我的濫code以後,給了我幾個改善的方向

1. RepeatChar() 可以直接用 Enumerable.Repeat() 來取代,也會少一層 function    

        使用Enumerable.Repeat()在把return值丟到String.Concate中
    
2. string upperChar = input[count].ToString().ToUpper(); 這一行可以再重構->擷取方法出來
            
        我同時也把lowerChart提取出來,並把upperChar和lowerChart和repactChar做成inline
        結果=>ToUpper(input[count]) + RepeatChar(count, ToLoAwer(input[count]));

3. List<String> output = new List<string>();
    ..
    output.Add(subOutput)
    可以改成 yield return + IEnumerable<T>
    
        yield return + IEnumerable在[**91的快快樂樂學LINQ系列**](https://dotblogs.com.tw/hatelove/2012/05/24/linq-enumerable-extension-method)裡有很深入仔細的教學,所以我也不需要在這多做介紹惹,我個人非常大推這個系列,我就是看了這系列文變成91迷der~
    

    
4. naming 還有改善空間

        因為實在不好想名子(我太爛惹,所以能inline的我都inline惹XD~當然還是要在inline後,讀code的人能夠懂那行在做甚麼為前提

心得:其實前面3點,在Dojo時91就有親自示範,我當時忙對著這些神操作表示驚嘆而來不急把它們給記下來XD(~~明明就是看過就忘惹~~