单元测试及先行测试开发
 
2008-12-04 作者:Eric Gunnerson 来源:Microsoft
 

摘要 :摘要 :Eric Gunnerson解释了先行测试开发的概念,并提供操作范例来说明如何将这个准则实际运用到应用程式中。 (列印共9页)

請由MSDN Online Code Center 下載integerlist.exe 範例檔 (英文)。 请由MSDN Online Code Center下载integerlist.exe范例档 (英文)。

如果您是屬於會看完我專欄結尾的自傳的少數民族,那麼就該知道我在當上程式設計部經理前,是C# 程式編譯的首席測試工程人員,而在這之前則負責C++ 程式編譯。如果您是属于会看完我专栏结尾的自传的少数民族,那么就该知道我在当上程式设计部经理前,是C#程式编译的首席测试工程人员,而在这之前则负责C++程式编译。 這就是為什麼我非常熱衷於分析程式碼和找出可能錯誤的原因。这就是为什么我非常热衷于分析程式码和找出可能错误的原因。

降低軟體錯誤率的方法之一是,擁有全力投注於軟體檢測工作的專業測試團隊。降低软体错误率的方法之一是,拥有全力投注于软体检测工作的专业测试团队。 但糟糕的是,如果有測試團隊,就會存在一種不肯花更多時間檢查程式碼穩定性的傾向,這連經驗豐富的程式開發人員也是一樣。但糟糕的是,如果有测试团队,就会存在一种不肯花更多时间检查程式码稳定性的倾向,这连经验丰富的程式开发人员也是一样。

軟體界常流傳一句話:「程式開發人員不該測試自己寫的程式碼。」 其中道理在於,因為程式開發人員對自己的程式碼非常瞭解,所以他們對於要如何正確地進行測試常常有偏見。软体界常流传一句话:「程式开发人员不该测试自己写的程式码。」其中道理在于,因为程式开发人员对自己的程式码非常了解,所以他们对于要如何正确地进行测试常常有偏见。 這個說法雖然蠻合理的,但卻忽略了一項重點-如果程式開發人員不自己測試程式碼,那怎麼能確定程式碼會如預期執行呢?这个说法虽然蛮合理的,但却忽略了一项重点-如果程式开发人员不自己测试程式码,那怎么能确定程式码会如预期执行呢?

答案很簡單:他們根本不確定。答案很简单:他们根本不确定。 而程式開發人員寫出不能執行或只有部份能用的程式碼是個很嚴重的問題。而程式开发人员写出不能执行或只有部份能用的程式码是个很严重的问题。 他們通常只針對少數情況,而不是各種情況來驗證自己的程式碼能否順利執行。他们通常只针对少数情况,而不是各种情况来验证自己的程式码能否顺利执行。

發現錯誤发现错误

發現錯誤的情況如下:发现错误的情况如下:

  1. 程式開發人員剛寫好程式碼時。程式开发人员刚写好程式码时。
  2. 程式開發人員想辦法讓程式碼順利執行時。程式开发人员想办法让程式码顺利执行时。
  3. 由團隊其他程式開發或測試人員發現。由团队其他程式开发或测试人员发现。
  4. 進行規模較大的產品測試時。进行规模较大的产品测试时。
  5. 由使用者發現。由使用者发现。

如果在第1 個情況下就發現錯誤,要修正很容易,成本也很低。如果在第1个情况下就发现错误,要修正很容易,成本也很低。 越到後面才發現,所付出的成本就越高,而且要修正使用者發現的錯誤,可能得花上100 或1000 倍的成本。越到后面才发现,所付出的成本就越高,而且要修正使用者发现的错误,可能得花上100或1000倍的成本。 更別提使用者通常還必須等到下個版本出來時問題才能解決。更别提使用者通常还必须等到下个版本出来时问题才能解决。

最理想的狀況是,程式開發人員在寫程式碼時就找出所有的錯誤。最理想的状况是,程式开发人员在写程式码时就找出所有的错误。 要找出錯誤,必須準備好在寫程式碼時可以執行的測試。要找出错误,必须准备好在写程式码时可以执行的测试。 現在就為您介紹撰寫測試的妙方。现在就为您介绍撰写测试的妙方。

先行測試開發先行测试开发

先行測試開發就是在寫程式碼「之前」先行測試。先行测试开发就是在写程式码「之前」先行测试。 所有測試都沒問題的話,就表示程式碼可以順利執行,並且還能繼續確認後來加入的新功能是否穩定。所有测试都没问题的话,就表示程式码可以顺利执行,并且还能继续确认后来加入的新功能是否稳定。

這個概念是Kent Beck 在1990 年代為Smalltalk 寫Smalltalk Unit 時首創。这个概念是Kent Beck在1990年代为Smalltalk写Smalltalk Unit时首创。 過去多年來,單元測試公用程式都已經可以在大多數環境中執行,其中一個很好用的.NET Framework 公用程式叫nUnit (英文)。过去多年来,单元测试公用程式都已经可以在大多数环境中执行,其中一个很好用的.NET Framework公用程式叫nUnit (英文)。

範例范例

我稍候會寫一個IntegerList類別,來說明先行測試開發的用法。我稍候会写一个IntegerList类别,来说明先行测试开发的用法。 這是原本儲存整數的ArrayList變型,因此沒有Boxing 和Unboxing 的負荷。这是原本储存整数的ArrayList变型,因此没有Boxing和Unboxing的负荷。

第一個步驟是建立主控台專案,並加入IntegerList.cs原始程式檔。第一个步骤是建立主控台专案,并加入IntegerList.cs原始程式档。 要連結nUnit 架構,我需要加入nUnit 架構的參考。要连结nUnit架构,我需要加入nUnit架构的参考。 在我的電腦上,該參考位於d:\program files\nUnit v2.0\bin在我的电脑上,该参考位于d:\program files\nUnit v2.0\bin

第二步是花點時間構想如何測試這個類別。第二步是花点时间构想如何测试这个类别。 這有點類似決定類別的功能,但重點則放在特定用法(在清單中加上值1,再檢查是否成功),而不是功能(將項目加入清單)。这有点类似决定类别的功能,但重点则放在特定用法(在清单中加上值1,再检查是否成功),而不是功能(将项目加入清单)。 若要建置類別,請先準備好一份要用的測試清單:若要建置类别,请先准备好一份要用的测试清单:

  1. 測試能否建構。测试能否建构。
  2. 在清單中加入2 個整數,並確定計算結果和項目都正確。在清单中加入2个整数,并确定计算结果和项目都正确。
  3. 用更多項目重做一次。用更多项目重做一次。
  4. 將清單轉換成字串。将清单转换成字串。
  5. 使用foreach來列舉清單。使用foreach来列举清单。

這個範例有點反常,因為一開始我就很清楚要類別執行什麼工作。这个范例有点反常,因为一开始我就很清楚要类别执行什么工作。 但大部份的類別都是逐步建置,而測試也應該隨類別一起增加。但大部份的类别都是逐步建置,而测试也应该随类别一起增加。

現在就開始吧。现在就开始吧。 先建立用來存放所有測試的C# 類別,檔名為IntegerListTest.cs先建立用来存放所有测试的C#类别,档名为IntegerListTest.cs 這是第一個測試檔案:这是第一个测试档案:

 using System; using System; 
 using System.Collections; using System.Collections; 
 using NUnit.Framework; using NUnit.Framework; 

 namespace IntegerList namespace IntegerList 
 { 
     /// <summary> /// <summary> 
     /// Summary description for IntegerClassTest. /// Summary description for IntegerClassTest. 
     /// </summary> /// </summary> 
     [TestFixture] 
     public class IntegerClassTest public class IntegerClassTest 
     { 
         [Test] 
         public void ListCreation() public void ListCreation() 
         { 
             IntegerList list = new IntegerList(); IntegerList list = new IntegerList(); 
             Assertion.AssertNotNull(list); 
         } 
     } 
 } 

[TestFixture]屬性將此類別標記為測試類別,且[Test]屬性將ListCreation()方法標記為測試方法。 [TestFixture]属性将此类别标记为测试类别,且[Test]属性将ListCreation()方法标记为测试方法。 在這個方法中,我先建立一個清單,然後再用Assertion類別來測試物件已建立完成。在这个方法中,我先建立一个清单,然后再用Assertion类别来测试物件已建立完成。

啟動nUnit GUI 測試程式、開啟可執行檔,然後執行測試。启动nUnit GUI测试程式、开启可执行档,然后执行测试。 結果顯示如下。结果显示如下。

点按此图即可将其放大

[圖1] nUnit GUI 顯示測試結果 [图1] nUnit GUI显示测试结果

這表示所有的測試都通過了。这表示所有的测试都通过了。 現在我想加入一些實際功能。现在我想加入一些实际功能。 首先,我希望能將整數加入清單。首先,我希望能将整数加入清单。 下列是相關測試:下列是相关测试:

         [Test] [Test] 
         public void TestSimpleAdd() public void TestSimpleAdd() 
         { 
             IntegerList list = new IntegerList(); IntegerList list = new IntegerList(); 
             list.Add(5); 
             list.Add(10); 
             Assertion.AssertEquals(2, list.Count); Assertion.AssertEquals(2, list.Count); 
             Assertion.AssertEquals(5, list[0]); Assertion.AssertEquals(5, list[0]); 
             Assertion.AssertEquals(10, list[1]); Assertion.AssertEquals(10, list[1]); 
         } 

在這個測試中,我選擇同時測試兩件事:在这个测试中,我选择同时测试两件事:

  • 清單能保持正確的Count屬性。清单能保持正确的Count属性。
  • 清單能存放兩個項目。清单能存放两个项目。

有些提倡測試導向開發的人主張儘可能細分各種測試,但我認為測試計算結果而不測試項目數目很奇怪,所以我選擇這麼做。有些提倡测试导向开发的人主张尽可能细分各种测试,但我认为测试计算结果而不测试项目数目很奇怪,所以我选择这么做。

編譯此程式碼失敗,因為IntegerList類別沒有方法,所以我加入Stub 來進行編譯:编译此程式码失败,因为IntegerList类别没有方法,所以我加入Stub来进行编译:

         public int Count public int Count 
         { 
             get 
             { 
                 return -1; return -1; 
             } 
         } 

         public void Add(int value) public void Add(int value) 
         { 
         } 

         public int this[int index] public int this[int index] 
         { 
             get 
             { 
                 return -1; return -1; 
             } 
         } 

然後再重頭執行測試,而測試結果是紅色,表示測試失敗。然后再重头执行测试,而测试结果是红色,表示测试失败。 這是好現象,代表我的測試真的管用,而且還找到錯誤。这是好现象,代表我的测试真的管用,而且还找到错误。 那麼我就可以進行實作了。那么我就可以进行实作了。 先從簡單但費時的範例開始:先从简单但费时的范例开始:

         public int Count public int Count 
         { 
             get 
             { 
                 return elements.Length; return elements.Length; 
             } 
         } 

         public void Add(int value) public void Add(int value) 
         { 
             int newIndex; int newIndex; 
             if (elements != null) if (elements != null) 
             { 
                 int[] newElements = new int[elements.Length + 1]; int[] newElements = new int[elements.Length + 1]; 
                 for (int index = 0; index < elements.Length; for (int index = 0; index < elements.Length; 
                      index++)     
                 { 
                     newElements[index] = elements[index]; newElements[index] = elements[index]; 
                 } 
                 newIndex = elements.Length; newIndex = elements.Length; 
                 elements = newElements; elements = newElements; 
             } 
             else 
             { 
                 elements = new int[1]; elements = new int[1]; 
                 newIndex = 0; newIndex = 0; 
             } 
             elements[newIndex] = value; elements[newIndex] = value; 
         } 

         public int this[int index] public int this[int index] 
         { 
             get 
             { 
                 return elements[index]; return elements[index]; 
             } 
         } 

現在我已經完成一小部份類別,還剩下確保測試類別正確執行的測試,但測試完成的項目很少。现在我已经完成一小部份类别,还剩下确保测试类别正确执行的测试,但测试完成的项目很少。 接下來,我要寫一個能檢查1000 個項目的測試。接下来,我要写一个能检查1000个项目的测试。

         [Test] [Test] 
         public void TestOneThousandItems() public void TestOneThousandItems() 
         { 
             list = new IntegerList(); list = new IntegerList(); 

             for (int i = 0; i < 1000; i++) for (int i = 0; i < 1000; i++) 
             { 
                 list.Add(i); 
             } 

             Assertion.AssertEquals(1000, list.Count); Assertion.AssertEquals(1000, list.Count); 
             for (int i = 0; i < 1000; i++) for (int i = 0; i < 1000; i++) 
             { 
                 Assertion.AssertEquals(i, list[i]); Assertion.AssertEquals(i, list[i]); 
             } 
         } 

這個測試沒問題,所以不必做任何變更。这个测试没问题,所以不必做任何变更。

加入ToString() 方法加入ToString()方法

接著,我會加入程式碼來測試ToString()能否正確執行:接着,我会加入程式码来测试ToString()能否正确执行:

         [Test] [Test] 
         public void TestToString() public void TestToString() 
         { 
             IntegerList list = new IntegerList(); IntegerList list = new IntegerList(); 
             list.Add(5); 
             list.Add(10); 
             string t = list.ToString(); string t = list.ToString(); 
             Assertion.AssertEquals("5, 10", t.ToString()); Assertion.AssertEquals("5, 10", t.ToString()); 
         } 

結果失敗。结果失败。 下列是使其通過測試的程式碼:下列是使其通过测试的程式码:

         public override string ToString() public override string ToString() 
         { 
             string[] items = new string[elements.Length]; string[] items = new string[elements.Length]; 
             for (int index = 0; index < elements.Length; index++) for (int index = 0; index < elements.Length; index++) 
             { 
                 items[index] = elements[index].ToString(); items[index] = elements[index].ToString(); 
             } 
             return String.Join(", ", items); return String.Join(", ", items); 
         } 

啟用Foreach启用Foreach

很多使用者會很希望能Foreach 我的清單。很多使用者会很希望能Foreach我的清单。 作法是將IEnumerable實作在類別上,再定義一個實作IEnumerable的不同類別。作法是将IEnumerable实作在类别上,再定义一个实作IEnumerable的不同类别。 首先,測試執行:首先,测试执行:

         [Test] [Test] 
         public void TestForeach() public void TestForeach() 
         { 
             IntegerList list = new IntegerList(); IntegerList list = new IntegerList(); 
             list.Add(5); 
             list.Add(10); 
             list.Add(15); 
             list.Add(20); 

             ArrayList items = new ArrayList(); ArrayList items = new ArrayList(); 

             foreach (int value in list) foreach (int value in list) 
             { 
                 items.Add(value); 
             } 

             Assertion.AssertEquals("Count", 4, items.Count); Assertion.AssertEquals("Count", 4, items.Count); 
             Assertion.AssertEquals("index 0", 5, items[0]); Assertion.AssertEquals("index 0", 5, items[0]); 
             Assertion.AssertEquals("index 1", 10, items[1]); Assertion.AssertEquals("index 1", 10, items[1]); 
             Assertion.AssertEquals("index 2", 15, items[2]); Assertion.AssertEquals("index 2", 15, items[2]); 
             Assertion.AssertEquals("index 3", 20, items[3]); Assertion.AssertEquals("index 3", 20, items[3]); 
         } 

我也使IntegerList實作IEnumerable我也使IntegerList实作IEnumerable

         public IEnumerator GetEnumerator() public IEnumerator GetEnumerator() 
         { 
             return null; return null; 
         } 

這在測試時產生例外狀況。这在测试时产生例外状况。 為了能正確實作,我使用巢狀類別列舉值。为了能正确实作,我使用巢状类别列举值。

     class IntegerListEnumerator: IEnumerator class IntegerListEnumerator: IEnumerator 
     { 
         IntegerList    list; IntegerList list; 
         int index = -1; int index = -1; 

         public IntegerListEnumerator(IntegerList list) public IntegerListEnumerator(IntegerList list) 
         { 
             this.list = list; this.list = list; 
         } 
         public bool MoveNext() public bool MoveNext() 
         { 
             index++; 
             if (index == list.Count) if (index == list.Count) 
                 return(false); 
             else 
                 return(true); 
         } 
         public object Current public object Current 
         { 
             get 
             { 
                 return(list[index]); 
             } 
         } 
         public void Reset() public void Reset() 
         { 
             index = -1; index = -1; 
         } 
     } 

此類別傳遞指標給IntegerList物件,然後僅傳回物件項目。此类别传递指标给IntegerList物件,然后仅传回物件项目。

這樣清單就可Foreach,但可惜Current屬性的型別為物件,每個值都會經過封裝才會傳回。这样清单就可Foreach,但可惜Current属性的型别为物件,每个值都会经过封装才会传回。 解決方法是使用模式(Pattern) 架構方法,它看起來和目前的方法一模一樣,但卻以GetEnumerator()傳回實際類別(而非IEnumerator ),且此類別的Current屬性型別為int解决方法是使用模式(Pattern)架构方法,它看起来和目前的方法一模一样,但却以GetEnumerator()传回实际类别(而非IEnumerator ),且此类别的Current属性型别为int

但完成上述作業後,我想確認在不支援模式架構方法的語言環境中,還是可以使用介面架構方法。但完成上述作业后,我想确认在不支援模式架构方法的语言环境中,还是可以使用介面架构方法。 所以我複製最後寫的那個測試,並修改Foreach 以轉換成介面。所以我复制最后写的那个测试,并修改Foreach以转换成介面。

             foreach (int value in (IEnumerable) list) foreach (int value in (IEnumerable) list) 

只要稍作變更,清單就能在兩個案例中執行。只要稍作变更,清单就能在两个案例中执行。 請參閱範例程式碼,以取得詳細資訊和更多測試。请参阅范例程式码,以取得详细资讯和更多测试。

建議事項建议事项

我只花了大概一小時就寫完這篇文章的程式碼和文字。我只花了大概一小时就写完这篇文章的程式码和文字。 預先寫好測試的好處在於,既然已經很清楚該如何編寫類別才能通過測試,那寫起程式碼就較能得心應手。预先写好测试的好处在于,既然已经很清楚该如何编写类别才能通过测试,那写起程式码就较能得心应手。

這個方法最適合用在小規模、逐次累加的測試。这个方法最适合用在小规模、逐次累加的测试。 我鼓勵您在小型專案上嘗試。我鼓励您在小型专案上尝试。 先行測試開發是稱為「靈活方法學(Agile Methodology)」的一部份,有關「靈活程式開發方式(Agile Development)」的詳細資訊,請造訪http://www.agilealliance.com/home (英文)。先行测试开发是称为「灵活方法学(Agile Methodology)」的一部份,有关「灵活程式开发方式(Agile Development)」的详细资讯,请造访http://www.agilealliance.com/home (英文)。


火龙果软件/UML软件工程组织致力于提高您的软件工程实践能力,我们不断地吸取业界的宝贵经验,向您提供经过数百家企业验证的有效的工程技术实践经验,同时关注最新的理论进展,帮助您“领跑您所在行业的软件世界”。
资源网站: UML软件工程组织