NUnit 快速入门

注:本页是基于原来的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]属性。编译并运行-同样是黄色的状态条。