首先,列出的是第一個“電影租賃”案例的C#版初始代碼:
[csharp]
using System;
using System.Collections.Generic;
namespace CH01_MovieRentalHouse
{
public class Movie
{
public const int CHILDRENS = 2;
public const int REGULAR = 0;
public const int NEW_RELEASE = 1;
public string Title { get; private set; }
public int PriceCode { get; private set; }
public Movie(string title, int priceCode)
{
Title = title;
PriceCode = priceCode;
}
}
public class Rental
{
public Movie Movie { get; private set; }
public int DaysRented { get; private set; }
public Rental(Movie rented, int days)
{
Movie = rented;
DaysRented = days;
}
}
public class Customer
{
public string Name { get; private set; }
private List<Rental> Rentals = new List<Rental>();
public Customer(string name)
{
Name = name;
}
public void Add(Rental rental)
{
Rentals.Add(rental);
}
public string Statement()
{
double totalAmount = 0;
int frequentRenterPoints = 0;
string result = "Rental Record for " + Name + "\n";
foreach (Rental rental in Rentals)
{
double thisAmount = 0;
// determine amounts for each line
switch (rental.Movie.PriceCode)
{
case Movie.REGULAR:
thisAmount += 2;
if (rental.DaysRented > 2)
thisAmount += (rental.DaysRented - 2) * 1.5;
break;
case Movie.NEW_RELEASE:
thisAmount += rental.DaysRented * 3;
break;
case Movie.CHILDRENS:
thisAmount += 1.5;
if (rental.DaysRented > 3)
thisAmount += (rental.DaysRented - 3) * 1.5;
break;
}
// add frequent renter points
frequentRenterPoints++;
// add bonus for a two day new release rental
if (rental.Movie.PriceCode == Movie.NEW_RELEASE &&
rental.DaysRented > 1) frequentRenterPoints++;
// show figures for this rental
result += "\t" + rental.Movie.Title + "\t" + thisAmount.ToString() + "\n";
totalAmount += thisAmount;
}
// add footer lines
result += "Amount owed is " + totalAmount.ToString() + "\n";
result += "You earned " + frequentRenterPoints.ToString() + " frequent renter points";
return result;
}
}
}
在工作中,經常見到動辄數百,甚至上千行的方法。對於那些程序員們,我實在不知道他們擁有怎樣的技巧,才能工作在復雜如斯的代碼謎團中,同時還能(真的能麼?)保證代碼不出錯。以個人經驗及數據來說,結構良好的代碼中,類方法的長度約有40%為1-5行,50%為5到15行左右,還有10%為15-50行。雖然不能嚴格地說方法的代碼行數越少越好,但簡短且職責更單一的方法確實成為了我衡量代碼的重要標准之一。
《重構》一書中提到了一個重要概念:“每當我要進行重構的時候,第一個步驟永遠相同:我得為即將修改的代碼建立一組可靠的測試環境”。誠然,沒有測試也能進行重構,但沒有它的保障,大多數重構行為都會以失敗告終。所以,後面所有的重構,都會在C#的單元測試框架基礎上完成,而這也是《重構》中沒有給出的部分(相當重要的部分)。
首先,必須確定的是初始代碼的正確性,這是重構的基礎之一,如果原始代碼不正確,我們也就無法用它來得到可供參考的結果。那麼,第一步工作不是立刻大刀闊斧地對現有代碼進行重構,而是先使用正確的初始代碼運行幾分測試數據,得到相應的結果。為此,添加如下代碼:
[csharp]
class Program
{
static void Main(string[] args)
{
Movie braveHeart = new Movie("BraveHeart", Movie.NEW_RELEASE);
Movie godFather = new Movie("GodFather", Movie.REGULAR);
Movie wallE = new Movie("Wall-E", Movie.CHILDRENS);
Rental bh4DayRental = new Rental(braveHeart, 4);
Rental bh2DRental = new Rental(braveHeart, 2);
Rental we7DRental = new Rental(wallE, 7);
Rental gfRental = new Rental(godFather, 5);
Rental we3DRental = new Rental(wallE, 3);
Customer Charles = new Customer("Charles");
Charles.Add(bh4DayRental);
Charles.Add(gfRental);
Customer Jess = new Customer("Jess");
Jess.Add(bh4DayRental);
Jess.Add(we7DRental);
Jess.Add(bh2DRental);
Customer Rebecca = new Customer("Rebecca");
Rebecca.Add(we7DRental);
Rebecca.Add(we3DRental);
Customer Lucas = new Customer("Lucas");
Lucas.Add(bh2DRental);
Lucas.Add(we7DRental);
Lucas.Add(we3DRental);
Console.WriteLine(Charles.Statement());
Console.WriteLine(Jess.Statement());
Console.WriteLine(Rebecca.Statement());
Console.WriteLine(Lucas.Statement());
}
}
運行後,控制台輸出如下:
接著,我們要做的就是在這個數據的基礎上編寫測試用例。鑒於有些同學可能還不知道怎麼使用C#的單元測試框架,所以演示一次,後面若無特殊情況,一概略過。關於C#單元測試的更多使用方法,參見微軟的教程,或在google上搜索。
http://msdn.microsoft.com/zh-cn/library/ms379625(v=vs.80).aspx
1.在Statement方法上點擊鼠標右鍵,會出現如下菜單(因為我裝了VA插件,所以菜單可能不完全一致)
2.選中其中的創建單元測試,會出現下述對話框
3.選中待測試的Statement()方法後,點擊確定
4.在項目名稱一欄,輸入一個你喜歡的名字作為測試項目的名稱,在這裡我將測試項目命名為:Tests,然後創建該項目。解決方案中多出了下圖中的文件和工程
5.打開CustomerTest.cs文件,發現系統自動為我們添加了一個測試:
[csharp]
/// <summary>
///Statement 的測試
///</summary>
[TestMethod()]
public void StatementTest()
{
string name = string.Empty; // TODO: 初始化為適當的值
Customer target = new Customer(name); // TODO: 初始化為適當的值
string expected = string.Empty; // TODO: 初始化為適當的值
string actual;
actual = target.Statement();
Assert.AreEqual(expected, actual);
Assert.Inconclusive("驗證此測試方法的正確性。");
}
該測試沒什麼用處,我們要做的是清空它,並使用前面產生的標准數據來編寫第一個測試,結果參照如下:
[csharp]
[TestMethod()]
public void StatementForCharles()
{
Movie braveHeart = new Movie("BraveHeart", Movie.NEW_RELEASE);
Movie godFather = new Movie("GodFather", Movie.REGULAR);
Movie wallE = new Movie("Wall-E", Movie.CHILDRENS);
Rental bh4DayRental = new Rental(braveHeart, 4);
Rental bh2DRental = new Rental(braveHeart, 2);
Rental we7DRental = new Rental(wallE, 7);
Rental gfRental = new Rental(godFather, 5);
Rental we3DRental = new Rental(wallE, 3);
Customer charles = new Customer("Charles");
charles.Add(bh4DayRental);
charles.Add(gfRental);
string expected = "Rental Record for Charles\n"
+ "\tBraveHeart\t12\n"
+ "\tGodFather\t6.5\n"
+ "Amount owed is 18.5\n"
+ "You earned 3 frequent renter points";
Assert.AreEqual(expected, charles.Statement());
}
需要注意的是,該測試的部分代碼是直接copy自上面Main函數,雖然這(拷貝、粘貼)不是被推薦的行為,但我們最終的目的實際上是將原來Main中的人工測試搬移到自動測試中,所以是可以被接受的,畢竟搬移後沒有功能重復的代碼。
6.構造完該測試後,從下圖所示菜單中運行單元測試
短暫的編譯、運行後,IDE中出現了類似下面的結果窗口:
結果列中,綠色的小勾勾說明,我們的測試通過了,如果輸入有誤,則結果畫面如下:
7.初步了解怎樣添加自動測試後,我們接著為另外三個參考數據編寫相應的測試用例,代碼如下:
[csharp]
[TestMethod()]
public void StatementForJess()
{
Movie braveHeart = new Movie("BraveHeart", Movie.NEW_RELEASE);
Movie godFather = new Movie("GodFather", Movie.REGULAR);
Movie wallE = new Movie("Wall-E", Movie.CHILDRENS);
Rental bh4DayRental = new Rental(braveHeart, 4);
Rental bh2DRental = new Rental(braveHeart, 2);
Rental we7DRental = new Rental(wallE, 7);
Rental gfRental = new Rental(godFather, 5);
Rental we3DRental = new Rental(wallE, 3);
Customer jess = new Customer("Jess");
jess.Add(bh4DayRental);
jess.Add(we7DRental);
jess.Add(bh2DRental);
string expected = "Rental Record for Jess\n"
+ "\tBraveHeart\t12\n"
+ "\tWall-E\t7.5\n"
+ "\tBraveHeart\t6\n"
+ "Amount owed is 25.5\n"
+ "You earned 5 frequent renter points";
Assert.AreEqual(expected, jess.Statement());
}
[TestMethod()]
public void StatementForRebecca()
{
Movie braveHeart = new Movie("BraveHeart", Movie.NEW_RELEASE);
Movie godFather = new Movie("GodFather", Movie.REGULAR);
Movie wallE = new Movie("Wall-E", Movie.CHILDRENS);
Rental bh4DayRental = new Rental(braveHeart, 4);
Rental bh2DRental = new Rental(braveHeart, 2);
Rental we7DRental = new Rental(wallE, 7);
Rental gfRental = new Rental(godFather, 5);
Rental we3DRental = new Rental(wallE, 3);
Customer rebecca = new Customer("Rebecca");
rebecca.Add(we7DRental);
rebecca.Add(we3DRental);
string expected = "Rental Record for Rebecca\n"
+ "\tWall-E\t7.5\n"
+ "\tWall-E\t1.5\n"
+ "Amount owed is 9\n"
+ "You earned 2 frequent renter points";
Assert.AreEqual(expected, rebecca.Statement());
}
[TestMethod()]
public void StatementForLucas()
{
Movie braveHeart = new Movie("BraveHeart", Movie.NEW_RELEASE);
Movie godFather = new Movie("GodFather", Movie.REGULAR);
Movie wallE = new Movie("Wall-E", Movie.CHILDRENS);
Rental bh4DayRental = new Rental(braveHeart, 4);
Rental bh2DRental = new Rental(braveHeart, 2);
Rental we7DRental = new Rental(wallE, 7);
Rental gfRental = new Rental(godFather, 5);
Rental we3DRental = new Rental(wallE, 3);
Customer lucas = new Customer("Lucas");
lucas.Add(bh2DRental);
lucas.Add(we7DRental);
lucas.Add(we3DRental);
string expected = "Rental Record for Lucas\n"
+ "\tBraveHeart\t6\n"
+ "\tWall-E\t7.5\n"
+ "\tWall-E\t1.5\n"
+ "Amount owed is 15\n"
+ "You earned 4 frequent renter points";
Assert.AreEqual(expected, lucas.Statement());
}
8.雖然測試都完成了,但是每一個測試中,有相當多的關於創建Movie和Rental對象的重復代碼。本著“測試代碼也要保持整潔”的原則,在這一篇的最後,我們把這些重復的可共享代碼全部抽取為測試類的字段,得到完整的測試代碼如下:
[csharp]
using CH01_MovieRentalHouse;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Tests
{
[TestClass]
public class CustomerTest
{
private Movie braveHeart;
private Movie godFather;
private Movie wallE;
private Rental bh4DayRental;
private Rental bh2DRental;
private Rental we7DRental;
private Rental gfRental;
private Rental we3DRental;
public CustomerTest()
{
braveHeart = new Movie("BraveHeart", Movie.NEW_RELEASE);
godFather = new Movie("GodFather", Movie.REGULAR);
wallE = new Movie("Wall-E", Movie.CHILDRENS);
bh4DayRental = new Rental(braveHeart, 4);
bh2DRental = new Rental(braveHeart, 2);
we7DRental = new Rental(wallE, 7);
gfRental = new Rental(godFather, 5);
we3DRental = new Rental(wallE, 3);
}
[TestMethod]
public void StatementForCharles()
{
Customer charles = new Customer("Charles");
charles.Add(bh4DayRental);
charles.Add(gfRental);
string expected = "Rental Record for Charles\n"
+ "\tBraveHeart\t12\n"
+ "\tGodFather\t6.5\n"
+ "Amount owed is 18.5\n"
+ "You earned 3 frequent renter points";
Assert.AreEqual(expected, charles.Statement());
}
[TestMethod]
public void StatementForJess()
{
Customer jess = new Customer("Jess");
jess.Add(bh4DayRental);
jess.Add(we7DRental);
jess.Add(bh2DRental);
string expected = "Rental Record for Jess\n"
+ "\tBraveHeart\t12\n" www.2cto.com
+ "\tWall-E\t7.5\n"
+ "\tBraveHeart\t6\n"
+ "Amount owed is 25.5\n"
+ "You earned 5 frequent renter points";
Assert.AreEqual(expected, jess.Statement());
}
[TestMethod]
public void StatementForRebecca()
{
Customer rebecca = new Customer("Rebecca");
rebecca.Add(we7DRental);
rebecca.Add(we3DRental);
string expected = "Rental Record for Rebecca\n"
+ "\tWall-E\t7.5\n"
+ "\tWall-E\t1.5\n"
+ "Amount owed is 9\n"
+ "You earned 2 frequent renter points";
Assert.AreEqual(expected, rebecca.Statement());
}
[TestMethod]
public void StatementForLucas()
{
Customer lucas = new Customer("Lucas");
lucas.Add(bh2DRental);
lucas.Add(we7DRental);
lucas.Add(we3DRental);
string expected = "Rental Record for Lucas\n"
+ "\tBraveHeart\t6\n"
+ "\tWall-E\t7.5\n"
+ "\tWall-E\t1.5\n"
+ "Amount owed is 15\n"
+ "You earned 4 frequent renter points";
Assert.AreEqual(expected, lucas.Statement());
}
}
}
小結:這一篇並沒有涉及具體的重構內容,而是一些重構前的准備:
1.使用原始代碼構造參考數據
2.使用C#的單元測試框架為這些參考數據創建自動單元測試
至於這麼做的最重要的原因是:為後面的重構提供可自動完成的、可重復執行的檢驗標准
作者:virtualxmars