注:本页是基于原来的QuickStart.doc文档,你可以在早期的NUnit发布中找到它。已经指出它并不是一个非常好的TDD实例。尽管如此,我们仍然将它保留在文档中,因为它的的确确描述了使用NUnit的基础。我们会在以后的版本中重新审查或替换它。
让我们从一个简单的实例开始吧。假设我们正在编写一个空应用程序,并且我们有一个基本的领域类-Account。Account提供了储蓄,取款,以及转帐等操作。Account类可能如下:
namespace bank
{
public class Account
{
private float balance;
public void Deposit(float amount)
{
balance+=amount;
}
public void Withdraw(float amount)
{
balance-=amount;
}
public void TransferFunds(Account destination,
float amount)
{
}
public float Balance
{
get{ return balance;}
}
}
}
现在让我们为此类编写第一个测试-AccountTest。我们即将测试的第一个方法是TransferFunds。
namespace bank
{
using NUnit.Framework;
[TestFixture]
public class AccountTest
{
[Test]
public void TransferFunds()
{
Account source = new Account();
source.Deposit(200.00F);
Account destination = new Account();
destination.Deposit(150.00F);
source.TransferFunds(destination,
100.00F);
Assert.AreEqual(250.00F, destination.Balance);
Assert.AreEqual(100.00F, source.Balance);
}
}
}
我们注意到的第一件事情就是此类包含一个[TestFixture]属性与之关联-这是一种描述类包含测试代码的方法(此属性可以被继承)。此类必须为public,并且对于其超类没有任何限制。此类也必须有个一缺省的构造子。
此类包含一个唯一的方法-TransferFunds,而且有一个[Test]属性与之关联-它标志了该方法是一个测试方法。测试方法必须返回void,并且不能带有参数。在我们的测试方法中,我们对一个需要测试的对象进行了普通的初始化,执行以测试的业务方法,并且检查了业务对象的状态。Assert类定义了一组方法,这些方法用来检查前置条件,在我们的例子里,我们使用AreEqual方法保证在转帐之后,2个帐户都有正确的余额(本方法有许多重载方法,在本示例中的版本有如下参数:第一个参数是一个期望值,第二个参数是实际值)。
编译并运行此实例。假设你已经将你的测试代码编译为一个bank.dll。启动NUnit GUI(安装文件会在桌面和“Program
Files"上创建一个快捷方式)。在GUI启动之后,选择File->Open菜单,并指向bank.dll所在的路径,在”Open“对话框打开选择该文件。当bank.dll文件加载之后,你
会在左边的面板上看到一个测试树形结构 ,在右边会有一组状态。点击Run按钮,状态条以及测试树的TransferFunds节点会变红-我们的测试失败了。”Error
and Failures"面板显示如下信息:TransferFunds : expected <250>
but was <150> 而且,栈跟踪面板会报告测试代码中的失败之处:at bank.AccountTest.TransferFunds()
in C:\nunit\BankSampleTests\AccountTest.cs:line 17 。这正是我们期望的:测试失败是因为我们并没有实现TransferFunds方法。现在我们让它工作吧。不要关闭此GUI,返回你的IDE并修复此代码,让你的TransferFunds方法如下:
public void TransferFunds(Account destination, float amount)
{
destination.Deposit(amount);
Withdraw(amount);
}
现在,重新编译代码,再一次点击GUI上的按钮-状态条以及测试树变绿了。(注意GUI是如何为您重新加载程序集的;我们会一直打开GUI,并在IDE中继续编写代码,写出更多的测试)。
让我们在Account代码里加入一些错误的检查。我们为帐户加入最小的余额,保证银行可以继续让他们的钱可以支付最小额度的透支。在Account类里增加一个最小余额的属性:
private float minimumBalance = 10.00F;
public float MinimumBalance
{
get{ return minimumBalance;}
}
我们使用一个异常来描绘一个透支:
namespace bank
{
using System;
public class InsufficientFundsException : ApplicationException
{
}
}
在AccountTest类里加入一个新的方法:
[Test]
[ExpectedException(typeof(InsufficientFundsException))]
public void TransferWithInsufficientFunds()
{
Account source = new Account();
source.Deposit(200.00F);
Account destination = new Account();
destination.Deposit(150.00F);
source.TransferFunds(destination, 300.00F);
}
本测试处理[Test]属性,还有一个[ExpectedException ]属性与之关联-这是一种用来描述测试代码期望某种特定异常的方式。如果这种异常在执行的过程中没有抛出-测试就失败。编译你的代码并返回到GUI。在你编译测试代码的同时,GUI变灰,并且收紧测试树,因为测试还没有运行(当测试树结构改变时,GUI会观察测试的程序集的改变,并更新它自己-例如,加入新的测试等)。点击“Run”按钮-我们又有一个红色的状态条。我们会得到如下失败:
TransferWithInsufficentFunds : InsufficientFundsException was expected
让我们再一次修复Account代码,按如下方法修改TransferFunds:
public void TransferFunds(Account destination, float amount)
{
destination.Deposit(amount);
if(balance-amount<minimumBalance)
throw new InsufficientFundsException();
Withdraw(amount);
}
编译并运行测试-绿色的状态条。成功了!但是等等,看看我们刚才编写的代码,我们会发现银行可能在每个没有成功的转帐操作失去一笔钱。让我们编写一个测试来证明我们的疑虑,增加如下测试方法:
[Test]
public void TransferWithInsufficientFundsAtomicity()
{
Account source = new Account();
source.Deposit(200.00F);
Account destination = new Account();
destination.Deposit(150.00F);
try
{
source.TransferFunds(destination, 300.00F);
}
catch(InsufficientFundsException expected)
{
}
Assert.AreEqual(200.00F,source.Balance);
Assert.AreEqual(150.00F,destination.Balance);
}
我们正测试业务方法的事务属性-要么都成功,要么都失败。编译并运行-红条。OK,我们已经让$300.00蒸发了((1999.com
déjà vu?)-源帐户有一个正确余额150.00,但是目标帐户则是$450.00.我们如何修复?我们仅需要将最小余额检查调用放在更新的前面即可:
public void TransferFunds(Account destination, float amount)
{
if(balance-amount<minimumBalance)
throw new InsufficientFundsException();
destination.Deposit(amount);
Withdraw(amount);
}
如果Withdraw()方法抛出另外一个异常怎么办?我们应该在捕获代码段中执行一个追加的业务,或是依赖我们的事务管理器来恢复对象的状态?关于这点,我们需要回答一些问题,但不是现在。同时,我们应该对失败的测试最些什么呢?删除它?一个比较好的方式是暂时忽略它,在测试代码中加入如下属性:
[Test]
[Ignore("Decide how to implement transaction management")]
public void TransferWithInsufficientFundsAtomicity()
{
// code is the same
}
编译并运行-黄色的状态条。点击“Tests Not Run”,在列表里你会看到e bank.AccountTest.TransferWithInsufficientFundsAtomicity()
,而且带有测试忽略的原因:
看一下我们的测试代码,我们会发现某些重构是有顺序的。所有测试方法都共享一组通用的测试对象。我们将这个初始化代码提取到一个setup方法里,并在所有测试中重用它。我们测试类的重构版本如下:
namespace bank
{
using System;
using NUnit.Framework;
[TestFixture]
public class AccountTest
{
Account source;
Account destination;
[SetUp]
public void Init()
{
source = new Account();
source.Deposit(200.00F);
destination = new Account();
destination.Deposit(150.00F);
}
[Test]
public void TransferFunds()
{
source.TransferFunds(destination,
100.00f);
Assert.AreEqual(250.00F, destination.Balance);
Assert.AreEqual(100.00F, source.Balance);
}
[Test]
[ExpectedException(typeof(InsufficientFundsException))]
public void TransferWithInsufficientFunds()
{
source.TransferFunds(destination,
300.00F);
}
[Test]
[Ignore("Decide how to implement transaction
management")]
public void TransferWithInsufficientFundsAtomicity()
{
try
{
source.TransferFunds(destination,
300.00F);
}
catch(InsufficientFundsException
expected)
{
}
Assert.AreEqual(200.00F,source.Balance);
Assert.AreEqual(150.00F,destination.Balance);
}
}
}
尽管Init方法有一个通用的初始化代码,但是它返回一个void类型,没有参数。它标记为[SetUp]属性。编译并运行-同样是黄色的状态条。 |