注:本页是基于原来的QuickStart.doc文档,你可以在早期的NUnit发布中找到它。已经指出它并不是一个非常好的TDD实例。尽管如此,我们仍然将它保留在文档中,因为它的的确确描述了使用NUnit的基础。我们会在以后的版本中重新审查或替换它。
让我们从一个简单的实例开始吧。假设我们正在编写一个空应用程序,并且我们有一个基本的领域类-Aclearcase/" target="_blank" >ccount。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]属性。编译并运行-同样是黄色的状态条。