雖然由程式開發人員自己寫unit tests(單元測試)來測試自己寫的程式碼已經行之有年,但是大部分的unit tests都是寫在主要的程式碼已經設計好、寫好之後。大部分的程式開發人員都有相同的的經驗,在主要程式碼寫好之後再來加入unit test是一項困難的工作,而且在時間的壓力之下unit test通常是第一個被跳過的步驟。
這篇文章要介紹的test-driven development (tdd,測試驅動開發方法),其主要目的就是試圖要解決這一個問題,並且讓程式開發人員可以因此寫出更高品質的,完整測試過的程式碼。其方法就是把整個程序反轉過來,在寫主要程式碼之前就要把unit tests寫好。tdd是所謂的extreme programming(xp,終極程式寫作)裡面所提到主要practices(實務、實作)之一,在java陣營中採用tdd的程式開發人員為數不少,在.net的陣營中則還只停留在很少數的文章談到如何使用tdd。
根據ron jeffries(xp陣營大將)的說法,所謂unit tests就是「…許多段的程式,寫這些程式的目的是用來成批執行(run in batch mode),以驗證我們所寫的classes(類別)。每一個unit test都負責送一個message給一個特定的class,並且驗證所傳回來的值是該test所預期的答案。」如果我們用比較實務的說法來說明的話,這段話的意思就是,你寫一個程式,你用這個程式來測試在你的主要程式中所有的classes的public interfaces(public介面)。unit tests跟所謂的requirement tests或acceptance tests不同,unit test測試的重點在於驗證你所寫的methods(子程序)所產生的結果,與你所預期的一模一樣。
這個說來簡單,做起來可能挑戰性很高。首先,你必須先要決定用什麼工具來寫這些unit tests。以往測試人員通常使用一些很大型、複雜的test engine(測試引擎),配合一些複雜的scripting languages(腳本語言、敘述性程式語言)來寫這些unit tests。這個可能只適合專業的測試人員或測試部門使用,對於由程式開發人員自己寫的unit tests來說,就不那麼適用了。事實上對於一般的程式設計人員來說,他們所需要的是一套的toolkit(工具組,程式庫),讓他們可以使用他們原本在程式開發過程就已經熟知的程式語言及ide(開發工具)來寫出這些unit tests來。
大部分這些年流行的unit testing frameworks(unit test開發框架)都是源自於由kent beck(xp 創始人) 所設計的unit test framework。這個unit test framework的背景是為所謂第一個xp專案(chrysler c3專案)所特別設計的。這個最起初的framework是用smalltalk寫成,並且經歷過多次的改版之後,到今天都還存在。在這個smalltalk版本的framework之後,kent和erich gamma(design pattern迷應該知道他是誰)又把這個framework給改版到java上,並且正式命名叫作junit。從此之後,這個framework就開始不斷被改版、並應用到各個不同的程式語言之上,其中包括了c++、vb、python、perl、以及許多不同的程式語言。
本文所討論的nunit 2.0是一個與它的先祖們(其他的framework)非常不一樣的版本。其他的xunit家族版本通常都有一個base class(基礎類別),你要寫的test classes(測試類別)都得inherit(繼承)自這個base class。除此之外,別無他法能夠讓你寫unit tests。不幸的是,這對很多的程式語言來說就造成很大的限制。比如說,java及c#就只能允許single inheritance(單一繼承)。也就是說,如果你想要refactor(重整)你的unit tests程式碼的話,你會遭遇到一些的限制;除非你引進一些複雜的inheritance hierarchies(類別繼承層級)。
有了.net之後一切又不同了,.net引進了一個新的程式開發的概念 ─ attributes(屬性),解決了這個煩人的問題。attributes讓你可以在你的程式碼之上再加入metadata(後設資料/母資料/超資料,描述程式碼的資料)。一般來說attributes不會影響到主要程式碼的執行,其功能是在你所寫程式碼之上添加了額外的資訊。attributes主要使用在documenting your code(註解你的程式碼),但是attributes也可以用來提供有關assembly的額外資訊,其他的程式就算沒有見過這個assembly,也可以使用這些資訊。這基本上就是nunit 2.0所作的事。在nunit 2.0裡面,有一個test runner application(負責執行unit tests的程式),這個test runner會掃描你已經compile(編譯)好的程式碼,並且從attribute裡面知道哪些classes是test classes,哪些methods是需要執行的test methods. 然後,test runner使用.net的reflection技術來執行這些test methods。因為這個緣故,你就不再需要讓你的test classes繼承自所謂的common base class。你唯一需要作的事,就是使用正確的attribute來描述你的test classes及test methods。
nunit提供了許多不同的attributes,讓你可以自由的寫你想要的unit tests。這些attributes可以用來定義test fixtures(見下一段解釋)、test methods,以及setup及teardown的methods(預備及善後工作的methods)。除此之外,還有其他的attributes可以來設定預期發生的exceptions,或者要求test runner跳過某些test method不執行。
testfixture attribute主要是用在class上,其作用的標誌該class含有需要執行的test methods。當你在一個class的定義裡加上這個attribute,test runner就會檢查該class,看看這個class是否含有test methods。
底下這段程式碼示範了如何使用testfixture attribute。(本文中所有程式碼都是用c#寫成,但是你應該知道,nunit也是用於其他的.net程式語言,包括vb.net。請參見nunit的相關文件。)
namespace unittestingexamples
{
using system;
using nunit.framework;
[testfixture]
public class sometests
{
}
}
使用textfixture attribute的class需要符合另一項唯一附加的限制,就是需要有一個public的default constructor(或者是沒有定義任何的constructor,這其實是相同的意思)。
test attribute主要用來標示在text fixture中的method,表示這個method需要被test runner application所執行。有test attribute的method必須是public的,並且必須return void,也沒有任何傳入的參數。如果沒有符合這些規定,在test runner gui之中是不會列出這個method的,而且在執行unit test的時候也不會執行這個method。
底下的程式碼示範了使用這個attribute的方法:
namespace unittestingexamples
{
using system;
using nunit.framework;
[testfixture]
public class sometests
{
[test]
public void testone()
{
// do something...
}
}
}
在寫unit tests的時候,有時你會需要在執行每一個test method之前(或之後)先作一些預備或善後工作。當然,你可以寫一個private的method,然後在每一個test method的一開頭或最末端呼叫這個特別的method。或者,你可以使用我們要介紹的setup及teardown attributes來達到相同的目的。
如同這兩個attributes的名字的意思,有setup attribute的method會在該textfixture中的每一個test method被執行之前先被test runner所執行,而有teardown attribute的method則會在每一個test method被執行之後被test runner所執行。一般來說,setup attribute及teardown attribute被用來預備一些必須的objects(物件),例如database connection、等等。
底下的範例示範了如何使用這兩個attributes:
namespace unittestingexamples
{
using system;
using nunit.framework;
[testfixture]
public class sometests
{
private int _somevalue;
[setup]
public void setup()
{
_somevalue = 5;
}
[teardown]
public void teardown()
{
_somevalue = 0;
}
[test]
public void testone()
{
// do something...
}
}
}
有的時候,你希望你的程式在某些特殊的條件下會產生一些特定的exception。要用unit test來測試程式是否如預期的產生exception,你可以用一個try..catch的程式區段來catch(捕捉)這個exception,然後再設一個boolean的值來證明exception的確發生了。這個方法固然可行,但是太花費功夫。事實上,你應該使用這個expectedexception attribute來標示某個method應該產生哪一個exception,如同下面的範例所示:
namespace unittestingexamples
{
using system;
using nunit.framework;
[testfixture]
public class sometests
{
[test]
[expectedexception(typeof(invalidoperationexception))]
public void testone()
{
// do something that throws an invalidoperationexception
}
}
}
如果上面的程式被執行的時候,如果一旦exception發生,而且這個exception的type(資料型別)是invalidoperationexception 的話,這個test就會順利通過驗證。如果你預期你的程式碼會產生多個exception的話,你也可以一次使用多個expectedexception attribute。但是,一個test method應該只測試一件事情,一次測試多個功能是不好的做法,你應該儘量避免之。另外,這個attributes並不會檢查inheirtance的關係,也就是說,如果你的程式碼產生的exception是繼承自invalidoperationexception 的subclass(子類別)的話,這個test執行的時候將不會通過驗證。簡而言之,當你使用這個attribute的時候,你要明確的指明所預期的exception是哪個type(資料型別)的。
這個attribute你大概不會經常用的,但是一但需要的時候,這個attribute是很方便使用的。你可以使用這個attribute來標示某個test method,叫test runner在執行的時候,略過這個method不要執行。使用這個ignore attribute的方法如下:
namespace unittestingexamples
{
using system;
using nunit.framework;
[testfixture]
public class sometests
{
[test]
[ignore("we're skipping this one for now.")]
public void testone()
{
// do something...
}
}
}
如果你想要暫時性的comment out一個test method的話,你應該考慮使用這個attribute。這個attribute讓你保留你的test method,在test runner的執行結果裡面,也會提醒你這個被略過的test method的存在。
除了以上所提到的這些用來標示測試程式所在的attributes之外,nunit還有一個重要的class你應該要知道如何使用。這個class就是assertion class。assertion class提供了一系列的static methods,讓你可以用來驗證主要程式的結果與你所預期的是否一樣。底下的範例示範了如何使用assertion class:
namespace unittestingexamples
{
using system;
using nunit.framework;
[testfixture]
public class sometests
{
[test]
public void testone()
{
int i = 4;
assertion.assertequals( 4, i );
}
}
}
(我知道這段程式碼只是用來示範用的,但是這段程式應該很明白的示範了我的意思。)
好,現在我們已經討論過寫unit tests的基本步驟及方法,現在讓我們來看看如何執行你所寫的unit tests。事實上非常簡單。nunit裡面有兩個已經寫好的test runner applications:一個是視窗gui程式,一個是console xml(命令列)程式。你可以自由選擇你所喜歡的方式,基本上是沒有什麼差別的。
如果你要使用視窗gui的test runner app,你只需要執行該程式,然後告訴它你要執行的test method所在的assembly位置。這個包含有你所寫test methods的assembly是那一個class library(或是executable,*.dll或*.exe) assembly,其中含有前面談到的test fixtures。當你告訴test runner你的assembly所在的位置,test runner會自動load這個asembly,然後把所有的class及test methods都列在視窗的左欄。當你按下’run’按鍵時,你就會自動執行所有列出來的test methods。你也可以double click其中的一個test class,或是一個test method之上,這樣會自動只執行該class或是該method。
底下是視窗gui test runner執行時的樣子:
在一些的情況下,特別是你想要在你自己寫的build script中加入unit testing的情況下,你大概不會使用gui test runner。在這個自動執行build script的情況下,你一般會把你build的結果貼在網頁,或寫入log file裡面存作紀錄,以供程式開發人員、經理或是客戶可以藉由檢查這個紀錄知道詳細情況。
在這個情況,你可以用nunit 2.0的console test runner application。這個test runner可以傳入assembly的位置當參數,其測試執行結果是一個xml字串。你可以用xslt或是css把這個xml結果轉換成html,或是其他你想要的格式。如果你需要用到這個功能的話,請查看nunit文件中有關console test runner application的資料。
說了這麼多,你已經知道怎麼寫unit tests了,對吧?阿哈,跟寫程式一樣,只知道語法,距離可以寫出好的程式還有一大段距離的。你還需要學習一些的技巧及方法,讓你可以真正寫出專業水準的應用程式出來。底下我們會談到一些幫助你開始的技巧及方法,但是你必須知道,唯一能夠讓你寫出夠水準的unit tests的方法只有一個,練習、練習、再練習。
如果你完全沒有聽過tdd的話,底下所說的東西你大概一時之間會無法接受。以往許多的程式設計人員,花了無數的時間及經歷,寫了許多的書籍及文章,告訴我們在寫程式碼之前要好好的做設計的功夫,然後才是寫出程式碼,最後則是小心地測試你寫的程式是否正確。好了,忘掉這一切吧,我接下來要告訴你的,是該上面所說的完全背道而馳的流程。
我們不再先設計、再寫程式碼、再作測試,我們整個把這個流程反轉過來,先寫測試碼。用另外的方式來說,我們絕對不寫任何一行的主要程式,除非我們先寫了測試碼,先執行了test methods,先有了一個應該會不通過驗證的測試。也就是說,整個寫程式的流程將會變成像這個樣子:
先寫一段unit test。
執行這個unit test。當然這個test連compile(編譯)都不能compile,因為你根本都還沒有寫任何的主要程式碼。 (我們把這個也當作test沒有通過)
寫你所能想到最簡單的程式碼,讓你的test可以compile。
現在再次執行你的unit test,你應該會得到驗證失敗的結果。 (如果不幸通過了的話,表示你的unit test根本沒有擊中要害,你的unit test是個不夠好的unit test) 。
現在你可以寫出你的主要程式,讓你的unit test可以順利的通過驗證。
再執行你的unit test,現在你的unit test應該已經順利通過了。 (如果還是不通過,你應該回到第5項,檢查看看你的程式碼哪裡出錯,修正之,然後再執行unit test) 。
現在你可以回到第1步驟,開始寫新功能的unit test!
事實上,當你在第5步驟的時候,你所用來寫程式碼的方法,就正好是所謂的coding by intention(目標、意圖導向)的方法。所謂的coding by intention就是說,你寫程式碼是由上而下寫的。其相對的方法就是由下而上的寫法,也就是說,當你寫一段程式的程式的時候,如果你發現你正在寫的class需要另一個a class提供一個foo method,這時你就先跳到a class去把這個method寫好,然後再回過頭來寫你之前正在寫的class。coding by intention則正好相反,當你寫程式的時候,你假裝a class已經有你所需要的foo method。等到你寫完你的程式要compile的時候,你的程式工具應該會告訴你你少了一個class或是一個method,這時候才來加入這個所需要的method。如同我們之前所說的,這是一件好事情,程式無法compile跟你的unit test不通過驗證是同樣的一回事。
當你用coding by intention的時候,你很清楚的用程式語言表達你想要作的事。這不但是幫助我們寫好unit tests,也讓我們所寫的程式更加的清楚、容易明白、容易debug(除錯),程式的設計也會更加的完善。在傳統程式開發方法中,測試只是用來幫助我們驗證我們所寫的程式沒有錯誤。但是在tdd裡面,unit tests可以幫助我們在寫程式碼之前,先清楚的定義我們所要的是什麼,我們的class應該要有哪些的功能。我絕不是說用tdd會比用傳統的測試方法還要輕鬆容易,但是我的經驗告訴我,其產生的結果的確對程式開發有極大的助益。
如果你已經聽過,也讀過有關extreme programming的書,下面的程式碼對你來說只是複習你已經知道的知識。如果tdd和extreme programming對你來說還很陌生,你可以看一下以下的範例。假設你要寫一個程式,這個程式可以讓你的使用者存錢在他的銀行帳戶裡面。現在,根據tdd的原則,當我們寫我們的bankaccount class之前,我們應該先從unit test著手。首先,我們先來寫我們的bankaccounttests class,我第一個想到的是我的bankaccount class應該可以接受存款並且告訴我新的餘額有多少。底下就是我的unit test程式碼:
namespace unittestingexamples.tests
{
using system;
using nunit.framework;
[testfixture]
public class bankaccounttests
{
[test]
public void testdeposit()
{
bankaccount account = new bankaccount();
account.deposit( 125.0 );
account.deposit( 25.0 );
assertion.assertequals( 150.0, account.balance );
}
}
}
現在我寫好了,我先試圖compile。哈,不能compile,當然,我都還沒有寫我的bankaccount class 呢。現在你知道,這就是test driven development的基本原則:除非你有一個不通過驗證的unit test,否則不要寫任何的程式碼。當然,不能compile也算是不通過驗證。
現在我可以來寫我的bankaccount class,我只有寫我所能想到最簡單、最陽春的程式碼,其目的只是讓我的unit test可以被compile:
namespace unittestingexamples.library
{
using system;
public class bankaccount
{
public void deposit( double amount )
{
}
public double balance
{
get { return 0.0; }
}
}
}
yes,這次compile已經過關了,接下來我就來用test runner執行我的unit test。嗯,沒有通過,test runner告訴我"testdeposit: expected: <150> but was <0>"。(譯者言,我在此不翻譯這個error message,畢竟這是你應該要看得懂的,除非你用的是中文版的nunit,否則學習看懂error message是必要的)。現在,接下來我就要來寫一些程式碼讓我的unit test能夠真正通過驗證,並產生我想要的結果:
namespace unittestingexamples.library
{
using system;
public class bankaccount
{
private double _balance = 0.0;
public void deposit( double amount )
{
_balance += amount;
}
public double balance
{
get { return _balance; }
}
}
}
ok,現在我的unit test過關了,我可以進行下一步的工作。(譯者言,事實上根據extreme programming或是kent beck書上的說法,你應該要先作refactoring(重整)的工作)。
當你在寫unit test的時候,你會碰到一些挑戰,其中一個就是要確保每一個test method都只有單單的測試一項單一的功能。但是,在一般的情況之下,你的test method所要測試的功能,往往會依賴其他的objects才能夠執行其功能。現在,如果你的test method測試這個功能,你真正測試的不只是這個功能,你也測試了另外的class。
如果這是一個問題,你可以用mock objects來幫助你區隔出你真正想要測試的功能。所謂的mock object,其主要的功能就是模擬別的object,讓你可以測試這一個被模擬的object是不是有被如預期般的正常使用。更要緊的是,使用mock objects還有以下的好處:
很容易就可以寫好
很容易就可以預備好你要的資料
執行速度極快
產生的結果是可預期的
可以讓你驗證某個object是否正確的呼叫了該呼叫的method,以及是否依照正確的順序來呼叫這些methods。
下面的程式範例示範了一個典型的mock object使用例子。請注意,現在unit test變得更加清晰,更加容易讓人明白,而且執行這個test只會測試了我們想測試的程式碼,而不會牽扯到不相干的objects。
namespace unittestingexamples.tests
{
using dotnetmock;
using system;
[testfixture]
public class modeltests
{
[test]
public void testsave()
{
mockdatabase db = new mockdatabase();
db.setexpectedupdates(2);
modelclass model = new modelclass();
model.save( db );
db.verify();
}
}
}
如上所示,使用這個mockdatabase class的預備工作很容易,也讓我們很容易的就可以驗證是否save這個method呼叫了mockdatabase class。使用了這個mock object也讓我們不需要操心真正的資料庫是否運作正常。我們唯一知道的,就是當這個modelclass object執行save這個動作的時候,它內部會呼叫database物件的update method兩次。我們的unit test就是要驗證是否真的有呼叫update method兩次。所以我們先告訴mockdatabase object我們預期update method會被呼叫兩次,然後我們執行model.save(),最後驗證其結果。因為mockdatabase不牽涉到連結資料庫的問題,我們也就不需要擔心資料庫會因為執行測試被修改,或是如何預備有效的測試資料,…此類的問題。我們唯一需要關心的是,save method是否真的造成了update被呼叫兩次。
"when mock objects are used, only the unit test and the target domain code are real." -- endo-testing: unit testing with mock objects by tim mackinnon, steve freeman and philip craig.
(當你使用mock objects的時候,只有你的unit test以及你所要測試的程式碼是真實的東西。 ─ 引自endo-testing一文,作者tim mackinnon, steve freeman and philip craig)
測試你的business layer(企業邏輯層)程式碼,是一般程式開發人員在講到unit test的時候最常舉的例子。如果你小心地規劃及設計你的business layer,你的business layer應該是loosely coupled(與其他部分獨立)以及highly cohesive(內部緊密連結的)。就實務上的用語來說,所謂的coupling指的是你的class與其他的classes互相依賴的程度,如果coupling的程度越低的話,如果我們要修改其中某個class的話,我們就不需要擔心其他的class的功能會被影響到。從另一角度來看,所謂的cohrsive就是說你的class應該只負責單一的任務,不應該加入其他的不完全相干的功能。
如果你的business layer class library(企業邏輯層程式庫)是loosely coupled以及highly conhesive的話,要寫unit test應該是輕而易舉的事。你應該可以針對每一個business class(在business layer裡的class)都寫一個unit test class,並且,你應該可以輕鬆的針對每一個business class的public methods都寫出相對應的測試來。
對了,如果你發覺當你想要針對你的business class寫unit test時,卻發現困難重重,你也許要考慮作一些大格局的refactoring(重整)的工作。當然,如果你是先寫unit tests再寫business classes的話,你應該是不可能會落入到這個地步的。
當你著手想要寫user interface(使用者介面)的程式碼的時候,有一些問題就開始跑出來了。當然你可以想辦法讓你的user interface符合loosely coupled的要求,讓它與其他的class都沒有依賴關係。但是,所謂的user interface本質上就是會依賴於使用者,需要由使用者來驅動及驗證。那麼,到底要如何的來解決這個問題,讓我們可以測試我們的user interface呢?
這個答案的關鍵之處在於,我們應該要小心的把所謂的logic(程式執行的邏輯),與所謂的user interface(用來顯示資料)清楚的區分開來,讓user interface真正只負責顯示view(視界/觀點)的工作。有很多的pattern(設計模式)就是用來幫助我們做好這個工作,它們的名稱不盡相同(model-view-controller, model-view-presenter, doc-view,等等),但是都是同樣的目的。這些pattern的創始者都深切的體認到,把view及view所要作事的邏輯(也就是controller)區分開來,是一件很有助益的事。
那麼,我們要如何使用這些pattern來幫助我們寫unit test來測試user interface呢?我在這裡所示範的技巧是來自於michael feathers所寫的the humble dialog box一文。就其本質上來說,這個技巧就是讓你的view class(也就是user interface class)implement(實作)一個簡單的interface。這個interface定義了這個view class應該要顯示的資料,並且有getting/setting methods(存取資料的metods)。你的view class應該單單的只有負責把資料顯示給使用者看,當使用者作了一些動作時(例如按了一個按鍵),view class裡的event handler的責任就是單單的把這個動作交給controller來處理。
我們用一個範例來看,就會更加的清楚明白。假設我們要寫一個程式,其中一個視窗是要讓使用者鍵入自己的名字及social security number(社安號碼,類似身分證字號譯註)。這兩個欄位都必須確實填寫,所以我們需要檢查使用者是否輸入名字,以及社安號碼是否符合正確的格式。既然我們應該要先寫測試碼,我們就要遵守自己的規則:
[testfixture]
public class vitalscontrollertests
{
[test]
public void testsuccessful()
{
mockvitalsview view = new mockvitalsview();
vitalscontroller controller = new vitalscontroller(view);
view.name = "peter provost";
view.ssn = "123-45-6789";
assertion.assert( controller.onok() == true );
}
[test]
public void testfailed()
{
mockvitalsview view = new mockvitalsview();
vitalscontroller controller = new vitalscontroller(view);
view.name = "";
view.ssn = "123-45-6789";
view.setexpectederrormessage( controller.error_message_bad_name );
assertion.assert( controller.onok() == false );
view.verify();
view.name = "peter provost";
view.ssn = "";
view.setexpectederrormessage( controller.error_message_bad_ssn );
assertion.assert( controller.onok() == false );
view.verify()
}
}
如果你試圖要compile及build這段測試程式碼,你會看到一大堆的錯誤訊息。別緊張,這是因為我們都還沒有開始寫mockvitalsview以及vitalscontroller這兩個class的緣故。好吧,現在就讓我們來寫這兩個classes的原始架構。記得我們的規則,我們只要寫最簡單、可以compile就好的程式碼:
public class mockvitalsview
{
public string name
{
get { return null; }
set { }
}
public string ssn
{
get { return null; }
set { }
}
public void setexpectederrormessage( string message )
{
}
public void verify()
{
throw new notimplementedexception();
}
}
public class vitalscontroller
{
public const string error_message_bad_ssn = "bad ssn.";
public const string error_message_bad_name = "bad name.";
public vitalscontroller( mockvitalsview view )
{
}
public bool onok()
{
return false;
}
}
咻!現在我們的unit test已經成功的compile了,讓我們來試試看是否這些unit tests可以通過驗證。噗!test runner告訴我們有兩個tests沒有通過驗證。第一個是在在testsuccessful 裡面呼叫controller.onok的時候,因為傳回來的結果是false,而非我們所預期的true。第二個地方是在testfailed裡面呼叫view.verify的時候。
我們繼續遵照我們先寫測試碼的原則來作,現在我們需要的是想辦法讓我們的unit tests可以順利通過。如果只是要讓testsuccessful 通過就很簡單,但是要讓testfailed 也通過我們就必須要寫一些真正有用的程式碼了。比如說像這樣:
public class mockvitalsview : mockobject
{
public string name
{
get { return _name; }
set { _name = value; }
}
public string ssn
{
get { return _ssn; }
set { _ssn = value; }
}
public string errormessage
{
get { return _expectederrormessage.actual; }
set { _expectederrormessage.actual = value; }
}
public void setexpectederrormessage( string message )
{
_expectederrormessage.expected = message;
}
private string _name;
private string _ssn;
private expectationstring _expectederrormessage =
new expectationstring("expected error message");
}
public class vitalscontroller
{
public const string error_message_bad_ssn = "bad ssn.";
public const string error_message_bad_name = "bad name.";
public vitalscontroller( mockvitalsview view )
{
_view = view;
}
public bool onok()
{
if( isvalidname() == false )
{
_view.errormessage = error_message_bad_name;
return false;
}
if( isvalidssn() == false )
{
_view.errormessage = error_message_bad_ssn;
return false;
}
// all is well, do something...
return true;
}
private bool isvalidname()
{
return _view.name.length > 0;
}
private bool isvalidssn()
{
string pattern = @"^/d{3}-/d{2}-/d{4}$";
return regex.ismatch( _view.ssn, pattern );
}
private mockvitalsview _view;
}
在我們繼續下去之前,讓我們先暫停一下回頭看看這段程式碼。首先要注意的是,我們完全沒有更動到我們起初寫的unit test程式(這是為什麼我沒有把它們列在上面的原因)。我們所大幅更動的,是在mockvitalsview 以及vitalscontroller之上。我們先來看mockvitalsview。
在我們先前的例子裡面,mockvitalsview原本沒有繼承自任何的base class。現在為了簡化我們的工作,我們把它變成繼承自dotnetmock.mockobject。這一個mockobject class給我們一個verify method,這個method為我們作了所有的工作。它所擁有的神奇力量乃是來自於expectation classes 。我們使用expectation class來設定我們所預期mock object會產生的事。在這個例子裡,我們預期errormessage這個property會有一個特定的值。因為這個property是屬於string資料型態的,所以我們在我們的mock object裡面加入了一個叫做expectationstring的member。接下來我們寫了setexpectederrormessage 以及errormessage 這兩個method及property,讓它們使用這個expectationstring object。當我們在我們的測試碼中呼叫verify的時候,mockobject base class就會自動的檢查我們所預期的結果是否會發生,並且通知我們任何與預期不一致的地方。很酷吧。是不是啊?
另外一個被大幅更動的就是我們的vitalscontroller class。因為這是所有真正出力幹活的程式碼所在,我們知道大幅更動乃是在所難免。基本上,我們主要的邏輯,主要的程式碼都是在onok這個method裡面。在這裡,我們用了我們的view class所定義存取資料的method來讀取使用者所填寫的資料,然後如果資料有任何不符合的話,我們用errormessage這個property來把錯誤訊息傳回去。
所以,我們的工作已經告一段落囉?還早的咧。到目前為止,我們都只是在controller上面大興土木,我們只是用模擬的mock view來假裝我們有一個user interface。事實上,我們還沒有任何東西可以秀給使用者看呢! 別緊張,我們現在需要的就只是讓我們已經寫好的controller,可以連接到真實的view上面去。怎麼做呢?
首先,我們需要把mockvitalsview所需要implement的interface給抓出來。如果我們看一下vitalscontroller 以及vitalscontrollertests 的程式碼,我們就可以發現底下的這個interface應該就夠滿足我們的需要:
public interface ivitalsview
{
string name { get; set; }
string ssn { get; set; }
string errormessage { get; set; }
}
我們有了我們要的新interface,現在,我們可以把在controller裡面用到mockvitalsview的程式碼,改成使用ivitalsview。然後,我們就可以修改mockvitalsview,讓它 implements這個ivitalsview。當然,我們做了這個refactoring的動作之後,要趕快的給它執行一下我們的unit test,確保我們沒有作了任何的傻事。假設一切都正常,所有的測試都通過了,我們就可以真正開始來寫view了。在這個例子裡面我用了asp.net的網頁來作我們的view,你應該知道,你可以很容易的就寫一個windows form的view。
底下是我們的.aspx 檔案:
<%@ page language="c#" codebehind="vitalsview.aspx.cs"
autoeventwireup="false"
inherits="unittestingexamples.vitalsview" %>
<!doctype html public "-//w3c//dtd html 4.0 transitional//en" >
<html>
<head>
<title>vitalsview</title>
<meta name="generator" content="microsoft visual studio 7.0">
<meta name="code_language" content="c#">
<meta name=vs_defaultclientscript content="javascript">
<meta name=vs_targetschema content="http://schemas.microsoft.com/intellisense/ie5">
</head>
<body ms_positioning="gridlayout">
<form id="vitalsview" method="post" runat="server">
<table border="0">
<tr>
<td>name:</td>
<td><asp:textbox runat=server id=nametextbox /></td>
</tr>
<tr>
<td>ssn:</td>
<td><asp:textbox runat=server id=ssntextbox /></td>
</tr>
<tr>
<td> </td>
<td><asp:label runat=server id=errormessagelabel /></td>
</tr>
<tr>
<td> </td>
<td><asp:button runat=server id=okbutton text="ok" /></td>
</tr>
</table>
</form>
</body>
</html>
底下是asp.net的code-behind程式碼:
using system;
using system.web.ui.webcontrols;
using unittestingexamples.library;
namespace unittestingexamples
{
/// <summary>
/// summary description for vitalsview.
/// </summary>
public class vitalsview : system.web.ui.page, ivitalsview
{
protected textbox nametextbox;
protected textbox ssntextbox;
protected label errormessagelabel;
protected button okbutton;
private vitalscontroller _controller;
private void page_load(object sender, system.eventargs e)
{
_controller = new vitalscontroller(this);
}
private void okbutton_click( object sender, system.eventargs e )
{
if( _controller.onok() == true )
response.redirect("thankyou.aspx");
}
#region ivitalsview implementation
public string name
{
get { return nametextbox.text; }
set { nametextbox.text = value; }
}
public string ssn
{
get { return ssntextbox.text; }
set { ssntextbox.text = value; }
}
public string errormessage
{
get { return errormessagelabel.text; }
set { errormessagelabel.text = value; }
}
#endregion
#region web form designer generated code
override protected void oninit(eventargs e)
{
//
// codegen: this call is required by the asp.net web form designer.
//
initializecomponent();
base.oninit(e);
}
/// <summary>
/// required method for designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void initializecomponent()
{
this.load += new system.eventhandler(this.page_load);
okbutton.click += new system.eventhandler( this.okbutton_click );
}
#endregion
}
}
如你所見的,唯一在我們的程式碼裡們所需的,就是implement我們之前所提到的ivitalsview 這個interface。 並且把我們的的asp.net web controls連結到ivitalviews所定義的存取資料的methods。當然,我們需要確保我們的view class裡面有一個controller object,並且呼叫controller裡面的methods。如果我們用這個方法來寫user interface,你會發現寫view的過程變得非常容易。而且因為所有的工作都在controller裡面完成,你可以測試到你大部分的程式碼,你會對你的程式非常的有信心。
test-driven development是一個很有威力的工具,你應該立刻馬上就使用這個工具來提昇你程式的品質,以及你程式寫作的功力。使用tdd會讓你更加仔細思考你的程式是否設計完善,並且保證所有你的程式碼都是經過測試千垂百鍊的(譯註:我承認我在此誇張了點)。如果你沒有完善的unit test,要針對你現有的程式碼作refactoring是幾乎不可能的事。一但跳入tdd的洪流之中,絕大部分的人都不再回頭用以前的老方法寫程式。試試看,你會發現,嗯~,真的是粉棒的啦。
新闻热点
疑难解答