qileilove

          blog已經(jīng)轉(zhuǎn)移至github,大家請訪問 http://qaseven.github.io/

          TDD從何開始

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

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

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

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

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

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

            最簡單的游戲過程是什么呢?游戲產(chǎn)生4位數(shù),玩家一把猜中,You win,游戲結(jié)束。

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

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

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

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

            然后我們測試?yán)锏腉ame就變成這個樣子了:

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

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

          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產(chǎn)生一個4位數(shù),不妨假定是1234:

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

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

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

            然后玩家猜數(shù)字,第一次猜了1234:

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

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

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

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

          context.checking(new Expectations() {                  

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

            同理我們可以推出guesser的一個最簡實現(xiàn):

          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")));     
              }                                   

            對應(yīng)的outputPrinter可以做如下的微調(diào):

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

            最后別忘了啟動Expectation驗證:

          context.assertIsSatisfied();

            整個測試方法現(xiàn)在看起來應(yīng)該是這樣的:

          @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();                                                  
           }

            運(yùn)行測試,會看到下面的錯誤信息:

          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()還是空的呢!現(xiàn)在就讓測試指引著我們前行吧。

            先改一改我們的Game類,把需要依賴的協(xié)作對象作為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來產(chǎn)生一個4位數(shù):

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

            再跑測試,會發(fā)現(xiàn)仍然錯,但結(jié)果有變化,第一步已經(jīng)變綠了!

          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)

            下面應(yīng)該使用inputCollector來收集玩家的輸入:

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

            跑測試,錯但是結(jié)果進(jìn)一步好轉(zhuǎn),已經(jīng)有兩步可以通過了:

           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)

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

          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");            
          }

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

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

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

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

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

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

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

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

          導(dǎo)航

          統(tǒng)計

          常用鏈接

          留言簿(55)

          隨筆分類

          隨筆檔案

          文章分類

          文章檔案

          搜索

          最新評論

          閱讀排行榜

          評論排行榜

          主站蜘蛛池模板: 额尔古纳市| 延吉市| 北碚区| 康乐县| 洪泽县| 宁强县| 恩平市| 阿坝| 东港市| 华安县| 东莞市| 大邑县| 嘉义县| 宜兰市| 苍南县| 穆棱市| 土默特左旗| 威信县| 揭西县| 怀远县| 商南县| 永兴县| 卢氏县| 绍兴县| 沙湾县| 枣阳市| 白山市| 桂阳县| 河间市| 巫溪县| 库伦旗| 道孚县| 肇州县| 独山县| 朝阳市| 东方市| 宕昌县| 吴桥县| 泰顺县| 广德县| 霍邱县|