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


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

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

傳統nUnit測試示例

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

MSpec的引入--- AAA語法

Rhino Mock --- 我演我

AutoMock --- 懶的最高境界

得心應手武器庫:

  • nUnit

  • MSpec

  • Rhino Mock

  • AutoMocking

 本文所涉及使用的工具, 見前文: 我的.Net武器庫 ------ 新.Net架構必備工具列表

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

依賴注入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;
    }

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

看截圖:

test_report

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

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM