深入淺出裸測之道---單元測試的單元化
三層架構(gòu)之解耦和單元測試
業(yè)務(wù)域的簡單案例---構(gòu)造器賦值
傳統(tǒng)nUnit測試示例
壞味道?---重構(gòu)的提出
MSpec的引入--- AAA語法
Rhino Mock --- 我演我
AutoMock --- 懶的最高境界
得心應(yīng)手武器庫:
nUnit
MSpec
Rhino Mock
AutoMocking
本文所涉及使用的工具, 見前文: 我的.Net武器庫 ------ 新.Net架構(gòu)必備工具列表
三層架構(gòu)之解耦和單元測試
依賴注入DI很大程度的幫助測試單元化。這對層與層之間的依賴關(guān)系,幾乎是真理。
如對數(shù)據(jù)讀寫的依賴關(guān)系,用IRepository替換之后,所有用到IRepository的類,如Serivce這一層的ExamService,在測試時,只需要傳入一個Mock的IRepository類,就不需要使用真實(shí)的數(shù)據(jù)庫對它測試了.
我們的另外一層Controller也用到Service這一層,同樣我為Service這一層的實(shí)現(xiàn)也提出一個接口IExamService,在Controller的構(gòu)造器中傳入IExamService的Mock類。因此,很容易的讓測試關(guān)注于Controller本身的行為和功能。甚至可以在ExamService類實(shí)現(xiàn)之前,我們就可以測試和實(shí)現(xiàn)Controller類。這是依賴注入的優(yōu)勢。
這一整套分層,解耦和測試我們已經(jīng)實(shí)現(xiàn)了,并形成一個規(guī)范的過程和成形的框架。現(xiàn)在已經(jīng)簡單到按部就班,就能輕松完成,甚至后期都可以考慮自動生成這部分代碼。但這部分現(xiàn)在不是本文的重點(diǎn)。
業(yè)務(wù)域的簡單案例---構(gòu)造器賦值
當(dāng)我們的注意力轉(zhuǎn)移到業(yè)務(wù)域時,情景有了悄悄的改變。業(yè)務(wù)域中,類與類之間有更多更復(fù)雜的依賴關(guān)系。相比之下,三層之間反而簡單。
這里,把我正在做的考試(Exam)類做一個簡單的背景介紹。考試,對于身經(jīng)百戰(zhàn)的我們應(yīng)該不陌生了,讓我們好好分析,看看熟悉身影的陌生之面。另外,我這里考試更多是拿社會化考試作分析目標(biāo)。
一個考試有三個很重要的要素:考試代碼(考試定義);考區(qū)(北京考區(qū),湖南考區(qū));考試日期。這三個要素,唯一標(biāo)識一個考試,也就是說,同一個考區(qū),同一個考試定義在同日期,我就認(rèn)為是同一個考試。很簡單的邏輯,為了體現(xiàn)這個邏輯,我把這三個要素,放在考試類的構(gòu)造器中。為什么?任何一個要素的缺失,考試對象的存在都沒有任何含義,所以一開始構(gòu)造的時候,就要傳入。從另一個角度,考區(qū)+考試定義+日期是考試的業(yè)務(wù)ID,是唯一標(biāo)識,必須貫穿于業(yè)務(wù)對象的始終。
看代碼:
public class Exam
{
public Exam(District district, ExamDef exam_def, Date date)
{
District = district;
ExamDef = exam_def;
Date = date;
}
}
通過構(gòu)造器,從外部傳入三個對象后,把它們賦給考試的三不屬性,而這三個屬性是只讀, Private是為了給nHibernate和構(gòu)造器使用的。為什么?如前所說他們是業(yè)務(wù)動,在創(chuàng)建之后,再修改沒有任何含義。
看代碼:
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; }
}
傳統(tǒng)nUnit測試示例
好了,背景已經(jīng)足夠了。讓我們來針對這部分功能進(jìn)行測試。喂,等等,我們……現(xiàn)在有功能嗎?有!我測試的描述就是,
當(dāng)從構(gòu)造器鏈構(gòu)造考試類時,三個屬性應(yīng)該要賦相應(yīng)的值。
是的,足夠簡單使我們一目了然,也足夠復(fù)雜,我們需要用測試來保障它的功能。 1. 保證它被運(yùn)行---覆蓋測試;2. 保證它是按我的設(shè)計進(jìn)行的---行為測試。
看代碼:
[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);
}
}
引入三個中間變量和另外三個類的定義我就不在這羅嗦了。我的命名方式也曾為人病詬,也不在這辯解。只看實(shí)質(zhì)內(nèi)容:分別創(chuàng)建三個類的實(shí)例,用于測試,至于這三個類的具體內(nèi)容,我其實(shí)并不關(guān)心。所以用個詞Stub來表示我的不關(guān)心。DDD的核心理念之一:名符其實(shí)。最后,我的斷言只判斷屬性的值是否與構(gòu)造器傳入值相符。OK,完成!
壞味道?---重構(gòu)的提出
過一段時,間。我們再回頭看看這段測試,會有些小小的不舒服。特別,我們還有更多的類有類似的構(gòu)造器賦值功能,還有更多更復(fù)雜的功能等著我們?nèi)y試,我們在做商業(yè)軟件,不是嗎?隨著類似的測試更得越多。這些小小的不舒服會越積越大。
這面的測試有什么問題?
1. 測試有三部分:建立測試環(huán)境;調(diào)用被測功能,(測試的本體);斷言。上面的代碼,我甚至都已經(jīng)刻意用注釋分離出了這么三塊,但仍不是語法級別的分離。
2. 對第三方的類依賴較為嚴(yán)重,這是本文的重點(diǎn)---單元測試單元化。對Exam類來說ExamDef, District都是插足的第三者。
3. 測試代碼太多,被測的實(shí)際上只有三行,雖然這不是原則性的問題,但是本著更好,更快,更強(qiáng)的精神,這個問題也是值得解決的。
好了,你提出的問題已經(jīng)太多了,我沒辦法一下子解決。3個還多?是的,我們的口號是“只要一個好”。
MSpec的引入--- AAA語法
言歸正傳,讓我們本著選代和重構(gòu)的原則來把這些問題一個一個解決。是的,測試也需要重構(gòu),測試代碼還有bug呢?一點(diǎn)不奇怪。你沒碰到過?噢,因?yàn)槟愀静粚憸y試代碼。
關(guān)于測試的三段式,我曾經(jīng)看過有人確實(shí)在nUnit的框架下一步一步重構(gòu),形成良好了測試框架。這里我就不這么麻煩了,直接上工具M(jìn)Spec!測試的三段式,有個說法,叫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;
}
再看一看測試運(yùn)行的結(jié)果,就明了代碼即文檔的含義了。
看截圖:
從nUnit升級到MSpec,給人一種耳目一新的感覺。開始也許會有些不習(xí)慣。但是,一旦習(xí)慣之后再也不想回頭了。
Rhino Mock --- 我演我
好了,看看第二個問題。一開始,我們依乎不覺得這是個大問題,不就是直接創(chuàng)建一個依賴美嗎,創(chuàng)建就完了唄,一行代碼而已。仍然,需要提醒注意,我們是在做商業(yè)軟件。一旦展開了,一個類不可能只是一、兩個類,特別是間接關(guān)聯(lián)的,會更多,拔出蘿卜帶出泥。就拿這個考試類來說,在我們的實(shí)際項(xiàng)目中,它還有考試科目列表屬性,還通過報考類與考生有間接聯(lián)系。而報考類又與訂單類,事務(wù)類有交互有關(guān)系。考慮所有這些級聯(lián)關(guān)系,難道我為了測試這個構(gòu)造賦值功能把所有的類全部創(chuàng)建出來?
再進(jìn)一步思考,我們會給出一個自然的解決方案,把考區(qū)類,考試定義類抽象出兩個接口來,構(gòu)造器傳入接口定義,而不是類本身。這其實(shí)是對層與層之間依賴注入的一個模仿。但是,相信我,這個方向是另一個夢魘的入口。業(yè)務(wù)域和多層之間完全是不同的環(huán)境。不想太深入討論,可能獨(dú)立一篇文章都打不住。
幸好,我們有另一個工具Rhino Mock,能幫助我們解決類的模擬的問題。改造之后的測試代碼如下。唯一的影響是,你需要為被模擬的類,加入一個至少是protected的無參數(shù)構(gòu)造器。這其實(shí)不是個大問題,如果你同時在項(xiàng)目中使用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>();
};
//...此處省略的沒有修改的代碼
}
可以看到,這一次的重構(gòu),把考試代碼、考區(qū)代碼等,其實(shí)你根本不關(guān)心的信息已經(jīng)省略掉了。
AutoMocking --- 懶的最高境界
到這還不夠,最后一個問題是填飽我們肚子的最有一塊燒餅。
隆重介紹AutoMocking,自動模擬。當(dāng)你的測試類從AutoMock的Specification類繼承時,它會自動為你創(chuàng)建一個被測試對象subject,并且根據(jù)被測試對象構(gòu)建器的參數(shù)定義,全自動的創(chuàng)建模擬對象。而引用這些模擬對象的方式,
很簡單Dependency<ExamDef>,就是依賴注入的依賴這個詞。已經(jīng)不需要太多的解釋---名如其實(shí)。
再看代碼:
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>());
};
}
三行實(shí)現(xiàn)代碼,對應(yīng)三行測試代碼。簡潔的不能再簡潔了。
皓月碧空,漫野如洗,行往卓越的路上


浙公網(wǎng)安備 33010602011771號