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

来源:岁月联盟 编辑:zhu 时间:2005-07-15

簡介

雖然由程式開發人員自己寫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也讓我們不需要操心真正的資料庫是否