Getting Started with NUnit
如果你打算开始学习,到下载页面选择一个NUnit版本。安装页面包含了安装说明。
开始NUnit阅读Quick Start页面。验证了一个C#银行应用程序的开发过程。查看Samples部分例子,包括了VB.NET、J#以及托管C++。
使用哪种形式?
NUnit有两种不同方式来测试用例。
A.控制台:NUnit-Console.exe,可以最快的使用,但是不能进行交互。
B.GUI:NUnit.exe,是一个Windows窗体应用程序提供可视化的界面,同时能够可选择性是运行测试。
NUnit Quick Start
Note:本页面是基于原来早期NUnit版本的QuickStart.doc文件。但它不是测试驱动开发的好例子。然而,我们仍然保留这个文档是因为它说明了使用NUnit的基础知识。在将来的版本中我们会对它进行更新。
现在我们从一个简单的例子开始,源代码在这里。假设我们开发一个银行应用程序,我们有有一个基本的域类-Account。这个类支持存款、取款、转账业务。代码如下:
namespace Bank { public class Account { private decimal balance; public void Deposit(decimal amount) { balance += amount; } public void Withdraw(decimal amount) { balance -= amount; } public void TransferFunds(Account destination, decimal amount) { } public decimal 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(200m); Account destination = new Account(); destination.Deposit(150m); source.TransferFunds(destination, 100m); Assert.AreEqual(250m, destination.Balance); Assert.AreEqual(100m, source.Balance); } } }
这个类首先需要注意到是它包含一个TestFixture特性(Attribute翻译为特性),通过这种方式指出这个类包含测试代码(特性可以被继承)。这个类是public修饰的并且它的父类没有限制。这个类也有一个默认构造函数。
这个类唯一的方法--TransferFunds,它有一个Test特性来关联它,它指出这个方法是一个测试方法。测试方法返回类型必须为void而且没有参数。在测试方法中我们初始化需要的测试的对象,执行测试业务逻辑并检查业务对象的状态。Assert类定义了用户检查后知条件的方法集合,在我们的例子中使用AreEqual方法确保转账后两个账户收支平衡。Assert有几个重载版本:第一个参数是期望值,第二个参数是实际值。
编译并运行这个例子。假设你已经编译代码为bank.dll。启动NUnit GUI(安装包已经在你的电脑桌面上和"Program Files"文件夹创建了一个快捷方式),启动之后选择File->Open菜单选项,浏览bank.dll并在"Open"对话框中选择它。当加载bank.dll是你会在左边控制板看见一个树结构的测试用例,在右边面板有一个状态集合。点击运行按钮,测试树中状态条和TransferFunds节点会变红--测试失败了。 “Errors and Failures”面板会显示一下信息:
TransferFunds : expected <250> but was <150>
在右下角堆栈信息面板会显示测试代码失败的地方:
at bank.AccountTest.TransferFunds() in C:\nunit\BankSampleTests\AccountTest.cs:line 19
以上是预期结果。这个用例失败是因为我们还没有实现TransferFunds方法。现在我们对它进行完善。不要关闭GUI并回到IDE修改源码,把TransferFunds改变如下
public void TransferFunds(Account destination, decimal amount) { destination.Deposit(amount); Withdraw(amount); }
现在编译代码,重新点击运行按钮,状态栏和测试树会变绿。注意GUI是怎么重新自动加载程序集,整个过程我们会保持GUI与IDE中代码为打开状态,并且编写更多的代码。
现在添加一些错误来检查Account类。我们为账户添加最小余额来保证银行能够在你账户上收取费用。现在在Account类上添加最小余额属性:
private decimal minimumBalance = 10m; public decimal 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(200m); Account destination = new Account(); destination.Deposit(150m); source.TransferFunds(destination, 300m); }
这个测试方法添加了Test特性之外还添加了ExpectedException特性,通过这种方式指出测试代码执行的时候预期抛出一个异常类型实例。如果没有抛出异常那么测试失败。编译代码然后回到GUI。在编译代码的时候GUI会变灰色并且折叠测试树仿佛测试没有运行(当测试树结构改变时,GUI观察到测试程序集的修改并且进行更新就像添加了一个新的测试用例)。点击运行按钮,状态栏会再次变红并会得到如下失败信息:
TransferWithInsufficentFunds : InsufficientFundsException was expected
再次修正Account代码,将TransferFunds修改为如下:
public void TransferFunds(Account destination, decimal amount) { destination.Deposit(amount); if(balance-amount < minimumBalance) throw new InsufficientFundsException(); Withdraw(amount); }
编译并运行测试,状态栏百年绿,测试成功。但是看看我们刚才写的代码我们会看到账户在每个失败的转账操作中都支出了钱。写一个测试来确认怀疑,添加如下测试方法:
[Test] public void TransferWithInsufficientFundsAtomicity() { Account source = new Account(); source.Deposit(200m); Account destination = new Account(); destination.Deposit(150m); try { source.TransferFunds(destination, 300m); } catch(InsufficientFundsException expected) { } Assert.AreEqual(200m, source.Balance); Assert.AreEqual(150m, destination.Balance); }
我们测试业务方法的事物属性--所有的方法同时成功或者同时失败。
编译并运行--状态栏变红。我们从源账户中支取$300并且成功执行,但是目的账户显示$450.怎么来修复这个问题。我们可以在更新前查询源账户并验证余额是否小于最小余额:
public void TransferFunds(Account destination, decimal amount) { if(balance-amount < minimumBalance) throw new InsufficientFundsException(); destination.Deposit(amount); Withdraw(amount); }
如果Withdraw方法抛出另外一个异常会怎么样呢?我们可以在catch块进行恢复事物或者依靠事物管理器来恢复对象状态。我们会在某些时候回答这些问题但不是现在。但在这期间我们怎么处理失败的测试呢,移除吗?更好的方式是暂时的忽略它并添加如下的方法到测试方法:
[Test] [Ignore("Decide how to implement transaction management")] public void TransferWithInsufficientFundsAtomicity() { // code is the same }
编译并运行--状态栏变黄。点击“Tests Not Run” 选项卡,你会看见在列表中看见bank.AccountTest.TransferWithInsufficientFundsAtomicity()被忽略的原因。
仔细看我们的测试代码会发现进行了一些重构。所有的测试方法都共享一系列测试对象。我们提炼出初始化代码到一个Setup方法,在所有测试中进行重用。测试类重构后如下:
namespace Bank { using NUnit.Framework; [TestFixture] public class AccountTest { Account source; Account destination; [SetUp] public void Init() { source = new Account(); source.Deposit(200m); destination = new Account(); destination.Deposit(150m); } [Test] public void TransferFunds() { source.TransferFunds(destination, 100m); Assert.AreEqual(250m, destination.Balance); Assert.AreEqual(100m, source.Balance); } [Test] [ExpectedException(typeof(InsufficientFundsException))] public void TransferWithInsufficientFunds() { source.TransferFunds(destination, 300m); } [Test] [Ignore("Decide how to implement transaction management")] public void TransferWithInsufficientFundsAtomicity() { try { source.TransferFunds(destination, 300m); } catch(InsufficientFundsException expected) { } Assert.AreEqual(200m, source.Balance); Assert.AreEqual(150m, destination.Balance); } } }
注意初始化方法会有共享初始化代码。它返回值为void,没有实参,并被使用了Setup特性。编译并运行,状态栏变黄了。