qileilove

          blog已經轉移至github,大家請訪問 http://qaseven.github.io/

          TDD從何開始

          萬事開頭難。在TDD中,人們糾結最多的可能是這樣一個問題:如何寫第一個測試呢?實際上要視不同的問題而定。如果問題本身是一個算法求解,或者是一個大系統中的小單元,那么可以從最簡單、最直觀的情況出發,這樣有助于快速建立信心,形成反饋周期。但是在實際的開發中,很多時候我們拿到的就是一個“應用級”的需求:一個網上訂票系統,一個網上書店,憤怒的小鳥,諸如此類。此時,我們如何TDD呢?一種很自然的想法是:

            先對系統做簡單的功能分解,形成概念中的相互協作的小模塊。然后再從其中的一個小模塊開始(往往是最核心的業務模塊)TDD。我們把這種方式權且稱為inside-out,也就是從部分到整體。這種方式可能存在的風險是:即使各個部分都通過TDD的方式驅動出來,我們也不能保證它們一起協作就能是我們想要的那個整體。更糟糕的是,直到我們把各個部分完成之前,我們都不知道這種無法形成整體的風險有多大。因此這對我們那個“概念中模塊設計”提出了很高的要求,并且無論我們當前在實現哪個模塊,都必須保證那個模塊是要符合概念中的設計的。

            如果換一種思路呢?與其做概念中的設計,不如做真正的設計,通過寫測試的方式驅動出系統的各個主要模塊及其交互關系,當測試完成并通過,整個應用的“骨架”也就形成了。

            例如,現在假設我們拿到一個需求,要實現一個猜數字的游戲。游戲的規則很簡單,游戲開始后隨機產生4位不相同的數字(0-9),玩家在6次之內猜出這個4位數就算贏,否則就算輸。每次玩家猜一個4位數,游戲都會告訴玩家這個4位數與正確結果的匹配情況,以xAyB的形式輸出,其中x表示數字在結果中出現,并且出現的位置也正確,y表示數字在結果中出現但位置不正確。如果玩家猜出了正確的結果,游戲結束并輸出“You win”,如果玩家輸,游戲結束并輸出“You lose”。

            針對這樣一個小游戲,有人覺得簡單,有人覺得復雜,但無論如何我們都沒有辦法一眼就看到整個問題的解決方案。因此我們需要理解需求,分析系統的功能:這里需要一個輸入模塊,那里需要一個隨機數產生模塊,停!既然已經在做分析了,為什么不用測試來記錄這一過程呢?當測試完成的時候,我們的分析過程也就完成了。

            好吧,從何開始呢?TDD有一個很重要的原則-反饋周期,反饋周期不能太長,這樣才能合理的控制整個TDD的節奏。因此我們不妨站在玩家的角度,從最簡單的游戲過程開始吧。

            最簡單的游戲過程是什么呢?游戲產生4位數,玩家一把猜中,You win,游戲結束。

            現在開始寫這個測試吧。有一個游戲(Game),游戲開始(start):

          Game game =newGame(); game.start();

            等等,似乎少了什么,是的,為了產生隨機數,需要有一個AnswerGenerator;為了拿到用戶輸入,需要有一個InputCollector;為了對玩家的輸入進行判斷,需要有一個Guesser;為了輸出結果,需要有一個OutputPrinter。真的要一口氣創建這么多類,并一一實現它們嗎?還好有mock,它可以幫助我們快速的創建一些假的對象。這里我們使用JMock2:

          Mockery context = new JUnit4Mockery() {                       
              {                                              
                  setImposteriser(ClassImposteriser.INSTANCE);
              }                                              
          };                                                 
          final AnswerGenerator answerGenerator = context.mock(AnswerGenerator.class);
              

            然后我們測試里的Game就變成這個樣子了:

          Game game =newGame(answerGenerator, inputCollector, guesser, outputPrinter); game.start();

            注意到這里為了通過編譯,需要定義上面提到的幾個類,我們不妨以最快的方式給出空實現吧:

          public class AnswerGenerator {
             
          }

          public class InputCollector {
             
          }

          public class Guesser {
             
          }

          public class OutputPrinter {
             
          }

            以及為了通過編譯而需要的Game的最簡單版本:

          public class Game {
              public Game(AnswerGenerator generator, InputCollector inputCollector, Guesser guesser, OutputPrinter outputPrinter) {
                 
              }
             
              public void start() {
                 
              }
          }

            好了,下面可以走我們的那個最簡單的流程了。首先是由answerGenerator產生一個4位數,不妨假定是1234:

          context.checking(new Expectations() {   
              {                                   
                  one(answerGenerator).generate();
                  will(returnValue(
          "1234"));      
              } 
          });

            這里需要我們的generator有一個generate方法,我們給一個最簡單的空實現:

          public class AnswerGenerator {     public String generate() {         return null;     }  }

            然后玩家猜數字,第一次猜了1234:

          context.checking(new Expectations() {                   
                                                              
              // ...
                                                              
              {                                                   
                  one(inputCollector).guess();                    
                  will(returnValue("1234"));                      
              }                                                   

            為了使編譯通過我們給inputCollector加上一個空的guess方法:

          public class InputCollector {     public String guess() {         return null;     } }

            然后guesser判斷結果,由于完全猜對,因此返回4A0B:

          context.checking(new Expectations() {                  

              // ...                                               
                                                                  
              {                                                   
                  oneOf(guesser).verify(with(equal("1234")), with(equal("1234")));                    
                  will(returnValue("4A0B"));                      
              }                                                  
          }

            同理我們可以推出guesser的一個最簡實現:

          public class Guesser {     public String verify(String input, String answer) {         return null;     } }

            最后玩家贏,游戲輸出“You win”,game over:

          context.checking(new Expectations() {  

              // ...

              {                                   
                  oneOf(outputPrinter).print(with(equal("You win")));     
              }                                   

            對應的outputPrinter可以做如下的微調:

          public class OutputPrinter {     public void print(String result) {      } }

            最后別忘了啟動Expectation驗證:

          context.assertIsSatisfied();

            整個測試方法現在看起來應該是這樣的:

          @Test                                                                             
           public void should_play_game_and_win() {                                          
               Mockery context = new JUnit4Mockery() {                                       
                   {                                                                         
                       setImposteriser(ClassImposteriser.INSTANCE);                          
                   }                                                                         
               };                                                                            
               final AnswerGenerator answerGenerator = context.mock(AnswerGenerator.class);  
               final InputCollector inputCollector = context.mock(InputCollector.class);     
               final Guesser guesser = context.mock(Guesser.class);                          
               final OutputPrinter outputPrinter = context.mock(OutputPrinter.class);        
                                                                                             
               context.checking(new Expectations() {                                         
                   {                                                                         
                       one(answerGenerator).generate();                                      
                       will(returnValue("1234"));                                            
                   }                                                                         
                                                                                             
                   {                                                                         
                       one(inputCollector).guess();                                          
                       will(returnValue("1234"));                                            
                   }                                                                         
                                                                                             
                   {                                                                         
                       oneOf(guesser).verify(with(equal("1234")), with(equal("1234")));      
                       will(returnValue("4A0B"));                                            
                   }                                                                         
                                                                                             
                   {                                                                         
                       oneOf(outputPrinter).print(with(equal("You win")));                   
                   }                                                                         
               });                                                                           
                                                                                             
               Game game = new Game(answerGenerator, inputCollector, guesser, outputPrinter);
               game.start();                                                                 
                                                                                             
               context.assertIsSatisfied();                                                  
           }

            運行測試,會看到下面的錯誤信息:

          java.lang.AssertionError: not all expectations were satisfied

          expectations:
          expected once, never invoked: answerGenerator.generate(); returns "1234"
          expected once, never invoked: inputCollector.guess(); returns "1234"
          expected once, never invoked: guesser.verify("1234"); returns "4A0B"
          expected once, never invoked: outputPrinter.print("You win"); returns a default value
          at org.jmock.lib.AssertionErrorTranslator.translate(AssertionErrorTranslator.java:20)
          at org.jmock.Mockery.assertIsSatisfied(Mockery.java:196)
          at com.swzhou.tdd.guess.number.GameFacts.should_play_game_and_win(GameFacts.java:54)

            太好了,正是我們期望的錯誤!別忘了我們只是在測試中定義了期望的游戲流程,真正的game.start()還是空的呢!現在就讓測試指引著我們前行吧。

            先改一改我們的Game類,把需要依賴的協作對象作為Game的字段:

          private AnswerGenerator answerGenerator;
          private InputCollector inputCollector;
          private Guesser guesser;
          private OutputPrinter outputPrinter;

          public Game(AnswerGenerator answerGenerator, InputCollector inputCollector, Guesser guesser, OutputPrinter outputPrinter) {
               this.answerGenerator = answerGenerator;
               this.inputCollector = inputCollector;
               this.guesser = guesser;
               this.outputPrinter = outputPrinter;
          }

            然后在start方法中通過answerGenerator來產生一個4位數:

          public void start() {                          
              String answer 
          = answerGenerator.generate();
          }

            再跑測試,會發現仍然錯,但結果有變化,第一步已經變綠了!

          java.lang.AssertionError: not all expectations were satisfied
          expectations:
          expected once, already invoked 1 time: answerGenerator.generate(); returns "1234"
          expected once, never invoked: inputCollector.guess(); returns "1234"
          expected once, never invoked: guesser.verify("1234"); returns "4A0B"
          expected once, never invoked: outputPrinter.print("You win"); returns a default value
          at org.jmock.lib.AssertionErrorTranslator.translate(AssertionErrorTranslator.java:20)
          at org.jmock.Mockery.assertIsSatisfied(Mockery.java:196)
          at com.swzhou.tdd.guess.number.GameFacts.should_play_game_and_win(GameFacts.java:54)

            下面應該使用inputCollector來收集玩家的輸入:

          public void start() {                          
              String answer 
          = answerGenerator.generate();
              String guess 
          = inputCollector.guess();     
          }

            跑測試,錯但是結果進一步好轉,已經有兩步可以通過了:

           java.lang.AssertionError: not all expectations were satisfied
          expectations:
          expected once, already invoked 1 time: answerGenerator.generate(); returns "1234"
          expected once, already invoked 1 time: inputCollector.guess(); returns "1234"
          expected once, never invoked: guesser.verify("1234"); returns "4A0B"
          expected once, never invoked: outputPrinter.print("You win"); returns a default value
          at org.jmock.lib.AssertionErrorTranslator.translate(AssertionErrorTranslator.java:20)
          at org.jmock.Mockery.assertIsSatisfied(Mockery.java:196)
          at com.swzhou.tdd.guess.number.GameFacts.should_play_game_and_win(GameFacts.java:54)

            下面加快節奏,按照測試中的需求把剩下的流程走通吧:

          public void start() {                          
              String answer = answerGenerator.generate();
              String guess = inputCollector.guess();     
              String result = "";                        
              do {                                       
                 result = guesser.verify(guess, answer); 
              } while (result != "4A0B");                
              outputPrinter.print("You win");            
          }

            再跑測試,啊哈,終于看到那個久違的小綠條了!

            回顧一下這一輪從無到有、測試從紅到綠的小迭代,我們最終的產出是:

            1、一個可以用來描述游戲流程的測試(需求,文檔?)。

            2、由該需求推出的一個流程骨架(Game.start)。

            3、一堆基于該骨架的協作類,雖然是空的,但它們每個的職責是清晰的。

            經過這最艱難的第一步(實際上敘述的過程比較冗長,但反饋周期還是很快的),相信每個人都會對完整實現這個游戲建立信心,并且應該知道后面的步驟要怎么走了吧。是的,我們可以通過寫更多的骨架測試來進一步完善它(比如考慮失敗情況下的輸出,增加對用戶輸入的驗證等等),或者深入到每個小協作類中,繼續以TDD的方式實現每一個協作類了。無論如何,骨架已在,我們是不大可能出現大的偏差了。

          posted on 2012-06-26 09:58 順其自然EVO 閱讀(411) 評論(0)  編輯  收藏 所屬分類: 測試學習專欄

          <2012年6月>
          272829303112
          3456789
          10111213141516
          17181920212223
          24252627282930
          1234567

          導航

          統計

          常用鏈接

          留言簿(55)

          隨筆分類

          隨筆檔案

          文章分類

          文章檔案

          搜索

          最新評論

          閱讀排行榜

          評論排行榜

          主站蜘蛛池模板: 建平县| 武邑县| 镇宁| 北宁市| 开远市| 塔河县| 盈江县| 晋城| 泽普县| 象山县| 靖宇县| 全州县| 湘潭县| 雷山县| 仁化县| 施甸县| 和田县| 天镇县| 堆龙德庆县| 苍溪县| 德格县| 微山县| 吴桥县| 江阴市| 嘉峪关市| 安康市| 安阳县| 东乌珠穆沁旗| 行唐县| 贡觉县| 静乐县| 临桂县| 乐清市| 衡南县| 温泉县| 巍山| 苍南县| 将乐县| 麻城市| 东乡县| 兴国县|