首页 > 开发 > 综合 > 正文

网上发现的文章(测试驱动开发)

2024-07-21 02:17:03
字体:
来源:转载
供稿:网友

簡介

雖然由程式開發人員自己寫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的洪流之中,絕大部分的人都不再回頭用以前的老方法寫程式。試試看,你會發現,嗯~,真的是粉棒的啦。

发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表