qileilove

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

          深入淺出裸測之道---單元測試的單元化

           三層架構之解耦和單元測試

            依賴注入DI很大程度的幫助測試單元化。這對層與層之間的依賴關系,幾乎是真理。

            如對數據讀寫的依賴關系,用IRepository替換之后,所有用到IRepository的類,如Serivce這一層的ExamService,在測試時,只需要傳入一個Mock的IRepository類,就不需要使用真實的數據庫對它測試了.

            我們的另外一層Controller也用到Service這一層,同樣我為Service這一層的實現也提出一個接口IExamService,在Controller的構造器中傳入IExamService的Mock類。因此,很容易的讓測試關注于Controller本身的行為和功能。甚至可以在ExamService類實現之前,我們就可以測試和實現Controller類。這是依賴注入的優勢。

            這一整套分層,解耦和測試我們已經實現了,并形成一個規范的過程和成形的框架。現在已經簡單到按部就班,就能輕松完成,甚至后期都可以考慮自動生成這部分代碼。但這部分現在不是本文的重點。

            業務域的簡單案例---構造器賦值

            當我們的注意力轉移到業務域時,情景有了悄悄的改變。業務域中,類與類之間有更多更復雜的依賴關系。相比之下,三層之間反而簡單。

            這里,把我正在做的考試(Exam)類做一個簡單的背景介紹。考試,對于身經百戰的我們應該不陌生了,讓我們好好分析,看看熟悉身影的陌生之面。另外,我這里考試更多是拿社會化考試作分析目標。

            一個考試有三個很重要的要素:考試代碼(考試定義);考區(北京考區,湖南考區);考試日期。這三個要素,唯一標識一個考試,也就是說,同一個考區,同一個考試定義在同日期,我就認為是同一個考試。很簡單的邏輯,為了體現這個邏輯,我把這三個要素,放在考試類的構造器中。為什么?任何一個要素的缺失,考試對象的存在都沒有任何含義,所以一開始構造的時候,就要傳入。從另一個角度,考區+考試定義+日期是考試的業務ID,是唯一標識,必須貫穿于業務對象的始終。

            看代碼:

          public class Exam
             {
                 public Exam(District district, ExamDef exam_def, Date date)
                 {
                     District = district;
                     ExamDef = exam_def;
                     Date = date;
                 }
             }

            通過構造器,從外部傳入三個對象后,把它們賦給考試的三不屬性,而這三個屬性是只讀,Private是為了給nHibernate和構造器使用的。為什么?如前所說他們是業務動,在創建之后,再修改沒有任何含義。

            看代碼:

          public class Exam
              {
                  public Exam(District district, ExamDef exam_def, Date date)
                  {
                      District = district;
                      ExamDef = exam_def;
                      Date = date;
                  }
                  public virtual ExamDef ExamDef { get; private set; }
                  public virtual District District { get; private set; }
                  public virtual Date Date { get;private set; }
              }

           傳統nUnit測試示例

            好了,背景已經足夠了。讓我們來針對這部分功能進行測試。喂,等等,我們……現在有功能嗎?有!我測試的描述就是,

            當從構造器鏈構造考試類時,三個屬性應該要賦相應的值。

            是的,足夠簡單使我們一目了然,也足夠復雜,我們需要用測試來保障它的功能。1、保證它被運行---覆蓋測試;2、保證它是按我的設計進行的---行為測試。

            看代碼:

          [TestFixture] 
              public class when_create_an_exam 
              { 
                  [Test] 
                  public void it_should_assign_parameters_to_properties() 
                  { 
                      //Arrange 
                      var stub_exam_def = new ExamDef("98"); 
                      var stub_district = new District("01"); 
                      var stub_date = new Date(2011, 1, 1); 
            
                      //Action 
                      var subject = new Exam(stub_district, stub_exam_def, stub_date); 
            
                      //Assert 
                      Assert.AreEqual(stub_district,subject.District); 
                      Assert.AreEqual(stub_exam_def,subject.ExamDef); 
                      Assert.AreEqual(stub_date,subject.Date); 
                  } 
              }

            引入三個中間變量和另外三個類的定義我就不在這羅嗦了。我的命名方式也曾為人病詬,也不在這辯解。只看實質內容:分別創建三個類的實例,用于測試,至于這三個類的具體內容,我其實并不關心。所以用個詞Stub來表示我的不關心。DDD的核心理念之一:名符其實。最后,我的斷言只判斷屬性的值是否與構造器傳入值相符。OK,完成!

            壞味道?---重構的提出

            過一段時,間。我們再回頭看看這段測試,會有些小小的不舒服。特別,我們還有更多的類有類似的構造器賦值功能,還有更多更復雜的功能等著我們去測試,我們在做商業軟件,不是嗎?隨著類似的測試更得越多。這些小小的不舒服會越積越大。

            這面的測試有什么問題?

            1、測試有三部分:建立測試環境;調用被測功能,(測試的本體);斷言。上面的代碼,我甚至都已經刻意用注釋分離出了這么三塊,但仍不是語法級別的分離。

            2、對第三方的類依賴較為嚴重,這是本文的重點---單元測試單元化。對Exam類來說ExamDef, District都是插足的第三者。

            3、測試代碼太多,被測的實際上只有三行,雖然這不是原則性的問題,但是本著更好,更快,更強的精神,這個問題也是值得解決的。

            好了,你提出的問題已經太多了,我沒辦法一下子解決。3個還多?是的,我們的口號是“只要一個好”。

            MSpec的引入--- AAA語法

            言歸正傳,讓我們本著選代和重構的原則來把這些問題一個一個解決。是的,測試也需要重構,測試代碼還有bug呢?一點不奇怪。你沒碰到過?噢,因為你根本不寫測試代碼。

            關于測試的三段式,我曾經看過有人確實在nUnit的框架下一步一步重構,形成良好了測試框架。這里我就不這么麻煩了,直接上工具MSpec!測試的三段式,有個說法,叫AAA語法,分別是Arrange,Action,Assert。3A級語法,多酷!

           而MSpec用了自己的名詞,分別是Establish, Because, It。看看下面改造之后的測試代碼就清楚什么意思了。

            看代碼:

          public class When_create_an_exam_by 
            { 
                private Establish context = 
                    () => 
                        { 
                            stub_exam_def = new ExamDef("98"); 
                            stub_district = new District("01"); 
                            stub_date = new Date(2011, 1, 1); 
                        }; 
           
                private Because of = 
                    () => subject = new Exam(stub_district, stub_exam_def, stub_date); 
           
                private It should_assign_to_properties = 
                    () => 
                        { 
                            subject.District.ShouldEqual(stub_district); 
                            subject.ExamDef.ShouldEqual(stub_exam_def); 
                            subject.Date.ShouldEqual(stub_date); 
                        }; 
           
                private static ExamDef stub_exam_def; 
                private static District stub_district; 
                private static Date stub_date; 
                private static Exam subject; 
            }

            再看一看測試運行的結果,就明了代碼即文檔的含義了。

            看截圖:

            從nUnit升級到MSpec,給人一種耳目一新的感覺。開始也許會有些不習慣。但是,一旦習慣之后再也不想回頭了。

            Rhino Mock --- 我演我

            好了,看看第二個問題。一開始,我們依乎不覺得這是個大問題,不就是直接創建一個依賴美嗎,創建就完了唄,一行代碼而已。仍然,需要提醒注意,我們是在做商業軟件。一旦展開了,一個類不可能只是一、兩個類,特別是間接關聯的,會更多,拔出蘿卜帶出泥。就拿這個考試類來說,在我們的實際項目中,它還有考試科目列表屬性,還通過報考類與考生有間接聯系。而報考類又與訂單類,事務類有交互有關系。考慮所有這些級聯關系,難道我為了測試這個構造賦值功能把所有的類全部創建出來?

           再進一步思考,我們會給出一個自然的解決方案,把考區類,考試定義類抽象出兩個接口來,構造器傳入接口定義,而不是類本身。這其實是對層與層之間依賴注入的一個模仿。但是,相信我,這個方向是另一個夢魘的入口。業務域和多層之間完全是不同的環境。不想太深入討論,可能獨立一篇文章都打不住。

            幸好,我們有另一個工具Rhino Mock,能幫助我們解決類的模擬的問題。改造之后的測試代碼如下。唯一的影響是,你需要為被模擬的類,加入一個至少是protected的無參數構造器。這其實不是個大問題,如果你同時在項目中使用nHibernate的話,也會有類似的要求。

            看代碼:

          public class When_create_an_exam 
              { 
                  private Establish context = 
                      () => 
                          { 
                              stub_exam_def = MockRepository.GenerateMock<ExamDef>(); 
                              stub_district = MockRepository.GenerateMock<District>(); 
                              stub_date = MockRepository.GenerateMock<Date>(); 
                          }; 
              //...此處省略的沒有修改的代碼 
              }

            可以看到,這一次的重構,把考試代碼、考區代碼等,其實你根本不關心的信息已經省略掉了。

            AutoMocking --- 懶的最高境界

            到這還不夠,最后一個問題是填飽我們肚子的最有一塊燒餅。

            隆重介紹AutoMocking,自動模擬。當你的測試類從AutoMock的Specification類繼承時,它會自動為你創建一個被測試對象subject,并且根據被測試對象構建器的參數定義,全自動的創建模擬對象。而引用這些模擬對象的方式,

            很簡單Dependency<ExamDef>,就是依賴注入的依賴這個詞。已經不需要太多的解釋---名如其實。

            再看代碼:

          public class When_create_an_exam:Specification<Exam> 
              { 
                  private It should_assign_to_properties = 
                      () => 
                          { 
                              subject.District.ShouldEqual(DependencyOf<District>()); 
                              subject.ExamDef.ShouldEqual(DependencyOf<ExamDef>()); 
                              subject.Date.ShouldEqual(DependencyOf<Date>()); 
                          }; 
              }

            三行實現代碼,對應三行測試代碼。簡潔的不能再簡潔了。


          posted on 2013-03-26 11:16 順其自然EVO 閱讀(231) 評論(0)  編輯  收藏 所屬分類: 測試學習專欄

          <2013年3月>
          242526272812
          3456789
          10111213141516
          17181920212223
          24252627282930
          31123456

          導航

          統計

          常用鏈接

          留言簿(55)

          隨筆分類

          隨筆檔案

          文章分類

          文章檔案

          搜索

          最新評論

          閱讀排行榜

          評論排行榜

          主站蜘蛛池模板: 四平市| 于都县| 登封市| 抚松县| 库尔勒市| 城口县| 赣州市| 翁源县| 鹿邑县| 黄平县| 北京市| 五家渠市| 定结县| 临夏市| 东丰县| 榆社县| 德兴市| 永吉县| 丰原市| 隆林| 谢通门县| 鹤庆县| 越西县| 三台县| 邓州市| 普兰店市| 临汾市| 和硕县| 海口市| 蚌埠市| 观塘区| 宜丰县| 轮台县| 涟源市| 军事| 富锦市| 永泰县| 新丰县| 东源县| 岫岩| 五峰|