首先,列出的是第一个“电影租赁”案例的C#版初始代码:
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#的单元测试框架基础上完成,而这也是《重构》中没有给出的部分(相当重要的部分)。
首先,必须确定的是初始代码的正确性,这是重构的基础之一,如果原始代码不正确,我们也就无法用它来得到可供参考的结果。那么,第一步工作不是立刻大刀阔斧地对现有代码进行重构,而是先使用正确的初始代码运行几分测试数据,得到相应的结果。为此,添加如下代码:
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,然后创建该项目。解决方案中多出了下图中的文件和工程

/// <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("验证此测试方法的正确性。");
}
该测试没什么用处,我们要做的是清空它,并使用前面产生的标准数据来编写第一个测试,结果参照如下:
[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中的人工测试搬移到自动测试中,所以是可以被接受的,毕竟搬移后没有功能重复的代码。



[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对象的重复代码。本着“测试代码也要保持整洁”的原则,在这一篇的最后,我们把这些重复的可共享代码全部抽取为测试类的字段,得到完整的测试代码如下:
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"
+ "\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());
}
}
}
小结:这一篇并没有涉及具体的重构内容,而是一些重构前的准备: