在.NET環境中使用Test-Driven Development(測試驅動開發方法)
Test-Driven Development In .NET


翻譯前言:


目次


簡介

雖然由程式開發人員自己寫Unit Tests(單元測試)來測試自己寫的程式碼已經行之有年,但是大部分的Unit Tests都是寫在主要的程式碼已經設計好、寫好之後。大部分的程式開發人員都有相同的的經驗,在主要程式碼寫好之後再來加入Unit Test是一項困難的工作,而且在時間的壓力之下Unit Test通常是第一個被跳過的步驟。

這篇文章要介紹的Test-driven development (TDD,測試驅動開發方法),其主要目的就是試圖要解決這一個問題,並且讓程式開發人員可以因此寫出更高品質的,完整測試過的程式碼。其方法就是把整個程序反轉過來,在寫主要程式碼之前就要把Unit Tests寫好。TDD是所謂的Extreme Programming(XP,終極程式寫作)裡面所提到主要Practices(實務、實作)之一,在Java陣營中採用TDD的程式開發人員為數不少,在.NET的陣營中則還只停留在很少數的文章談到如何使用TDD。

何為Unit Tests(單元測試)?

根據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 Framework(NUnit 單元測試框架)簡介

本文所討論的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簡介

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簡介

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...
        }
    }
}

SetUp & Teardown Attributes簡介

在寫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...
        }
    }
}

ExpectedException Attributes簡介

有的時候,你希望你的程式在某些特殊的條件下會產生一些特定的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(資料型別)的。

Ignore Attributes簡介

這個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的存在。

NUnit Assertion Class簡介

除了以上所提到的這些用來標示測試程式所在的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 );
        }
    }
}

(我知道這段程式碼只是用來示範用的,但是這段程式應該很明白的示範了我的意思。)

執行你的Tests

好,現在我們已經討論過寫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的資料。

使用Test-Driven Development(測試驅動開發方法)

說了這麼多,你已經知道怎麼寫Unit Tests了,對吧?阿哈,跟寫程式一樣,只知道語法,距離可以寫出好的程式還有一大段距離的。你還需要學習一些的技巧及方法,讓你可以真正寫出專業水準的應用程式出來。底下我們會談到一些幫助你開始的技巧及方法,但是你必須知道,唯一能夠讓你寫出夠水準的Unit Tests的方法只有一個,練習、練習、再練習。

如果你完全沒有聽過TDD的話,底下所說的東西你大概一時之間會無法接受。以往許多的程式設計人員,花了無數的時間及經歷,寫了許多的書籍及文章,告訴我們在寫程式碼之前要好好的做設計的功夫,然後才是寫出程式碼,最後則是小心地測試你寫的程式是否正確。好了,忘掉這一切吧,我接下來要告訴你的,是該上面所說的完全背道而馳的流程。

我們不再先設計、再寫程式碼、再作測試,我們整個把這個流程反轉過來,先寫測試碼。用另外的方式來說,我們絕對不寫任何一行的主要程式,除非我們先寫了測試碼,先執行了test methods,先有了一個應該會不通過驗證的測試。也就是說,整個寫程式的流程將會變成像這個樣子:

  1. 先寫一段Unit Test。

  2. 執行這個Unit Test。當然這個Test連compile(編譯)都不能compile,因為你根本都還沒有寫任何的主要程式碼。 (我們把這個也當作Test沒有通過)

  3. 寫你所能想到最簡單的程式碼,讓你的Test可以compile。

  4. 現在再次執行你的Unit Test,你應該會得到驗證失敗的結果。 (如果不幸通過了的話,表示你的Unit Test根本沒有擊中要害,你的Unit Test是個不夠好的Unit Test) 。

  5. 現在你可以寫出你的主要程式,讓你的Unit Test可以順利的通過驗證。

  6. 再執行你的Unit Test,現在你的Unit Test應該已經順利通過了。 (如果還是不通過,你應該回到第5項,檢查看看你的程式碼哪裡出錯,修正之,然後再執行Unit Test) 。

  7. 現在你可以回到第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(重整)的工作)。

使用Mock Objects(模擬物件) – DotNetMock

當你在寫Unit Test的時候,你會碰到一些挑戰,其中一個就是要確保每一個test method都只有單單的測試一項單一的功能。但是,在一般的情況之下,你的test method所要測試的功能,往往會依賴其他的objects才能夠執行其功能。現在,如果你的test method測試這個功能,你真正測試的不只是這個功能,你也測試了另外的class。

如果這是一個問題,你可以用Mock Objects來幫助你區隔出你真正想要測試的功能。所謂的Mock Object,其主要的功能就是模擬別的Object,讓你可以測試這一個被模擬的object是不是有被如預期般的正常使用。更要緊的是,使用Mock Objects還有以下的好處:

  1. 很容易就可以寫好

  2. 很容易就可以預備好你要的資料

  3. 執行速度極快

  4. 產生的結果是可預期的

  5. 可以讓你驗證某個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

測試你的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(使用者介面)的程式碼的時候,有一些問題就開始跑出來了。當然你可以想辦法讓你的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的洪流之中,絕大部分的人都不再回頭用以前的老方法寫程式。試試看,你會發現,嗯~,真的是粉棒的啦。

其他Web上的資源

NUnit http://www.nunit.org/
csUnit http://www.csunit.org (NUnit的另外一個選擇)
DotNetMock http://sourceforge.net/projects/dotnetmock/
MockObjects http://www.mockobjects.com/
The Humble Dialog Box http://www.objectmentor.com/resources/articles/TheHumbleDialogBox.pdf
 


原作者之授權函

Hi,

I took over the translation of 'Test-Driven
Development in .NET' from Gary weeks ago. Attached is
the finished translation in word document. Following
is the author's authorization.

Thanks.

Harry Chou
Tuesday, September 30, 2003


--- 陳孟廷 <aymigo@mail2000.com.tw> wrote:
> From: "陳孟廷" <aymigo@mail2000.com.tw>
> Subject: Fw: question about Test-Driven Development
> In .NET
> To: yuheng98@yahoo.com
> Date: Thu, 25 Sep 2003 08:47:13 +0800 (CST)
>
> Hi
>
>    this is author's authorize, Thanks!
>
>
>                                                Gary
>
>
----------------------------------------------------------
> Gary,

> You have my permission if you will agree to the
> following:

> 1. Provide me with an electronic copy of the
> translation. PDF would be great.
> 2. Maintain my copyright and authorship. You may
> include "Translated by..." on the title page.
> 3. Include a link to "the author's site"
> (http://www.peterprovost.org)
> 4. Include a link to the original paper's PDF
>
(http://www.peterprovost.org/Test-Driven%20Development%20In%20.NET.pdf)

> If you agree to all this, then go ahead and do it.
> Please send me the PDF when you are done.

> Thanks!

>
>
--------------------------------------------------------------------------------
>

> Peter Provost
> Bespoke Technologies, Inc
> Phone: 303.293.9000 ext. 19
> Email: peter.provost@bespoketechnologies.com
> Web: http://www.bespoketechnologies.com
>
> Check out my personal weblog about .NET and other
> development technologies at
> http://www.peterprovost.org/.
> -----Original Message-----
> From: Gary Chen [mailto:aymigo@ms18.hinet.net]
> Sent: Thursday, May 01, 2003 3:38 AM
> To: peter@provost.org
> Subject: Test-Driven Development In .NET
>
>
> Dear Peter,
>
>           I've read the articles "Test-Driven
> Development In .NET" on the web site and like it
>           very much. It will be great if I could
> translate it into traditional Chinese and
>           publish on our own site
> http://www.dotspace.idv.tw
>           to help those who want to explore unit
> test more.
>           I will, of course, declare the source and
> "hyperlink" those
>           to your original documents. I'll be very
> grateful if you could give your
>           authorization for me to do this.
>           Our web site contents about software
> engineering topics like XP, UML,
>           Rup, Design Patterns, Framework, those are
> all in traditional Chinese.
>           Regards,
>                                                    
>       Gary Chen
>                2003/5/1