在了解了NUnit的对象识别断言后(【Nunit入门系列讲座 4】NUnit断言- 对象识别断言),本想继续带大家深入了解Nunit的断言系统。不过,断言的种类很多,而内容又相对枯燥,为了不打击大家学习的兴趣,所以今天我们换个口味,来学习一个全新的内容——NUnit的异常测试。说到异常,其实很多朋友都有所耳闻,甚至很多朋友会对它有所忌讳,认为异常就意味着程序的错误,是一个坏消息的代名词。事实确实如此么?我们就来一起探讨下,并学会在NUnit中编写针对异常的测试。
其实不仅仅在.NET,早在C++中就有了异常机制。现在一个优秀的语言的标志之一,就是是否良好支持异常机制。异常顾名思义,就是异于正常,也就是不符合正常流程的事情发生了。那如何理解这段的标题呢?菩萨大人教导我们:“不怕念起,只怕觉迟”,就是说,不怕坏事情将要发生,就怕你不知道。我们在程序中的异常,就是我们在程序中发现错误的一种手段。有了异常,我们就可以先知先觉,在错误造成问题之前,把他们处理掉。下面我们看一个例子,顺便了解下.net中异常的定义和应用方法。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace ExceptionExample
{
class Exp
{
static void Main(string[] args)
{
int[] array1 = new int[] { 1, 3, 5, 7, 9 };
Console.WriteLine(array1[5]);
}
}
}
这个例子里定义了一个数组,包含五个元素,然后打印出这个数组的第六个元素。运行一下,看看会出现什么情况。
很显然,程序崩溃了。因为这是一个系统可以识别的异常,所以异常被显示了出来。这样的程序显然不能接受,那我们应该怎么样呢。最好是告诉用户,访问第6个元素是不可能的,因为我们的数组只有5个元素。这样用户看来,这个程序不是崩溃了,而是自己提了不合理的要求(不过他也不会脸红的)。修改代码如下
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace ExceptionExample
{
class Exp
{
static void Main(string[] args)
{
int[] array1 = new int[] { 1, 3, 5, 7, 9 };
try
{
Console.WriteLine(array1[5]);
}
catch (IndexOutOfRangeException ex)
{
Console.WriteLine("coule not access the sixth element,we only have " + array1.Length+" elements in arry1");
}
}
}
}
现在再次运行一下新的程序,看下结果如何
恩,现在好多了。大家从这个例子里就可以初步看出,异常机制是帮助我们发现程序中的一些错误情况,然后在我们的代码中,插入一些针对这些错误情况的处理。这个过程,就叫做异常捕获及处理。异常机制的实现原理较为复杂,我们在本教程中不做详述,但是可以把try想象成一个监控块,这个块内一旦出现了异常,就会被发现,然后跳转到异常对应的catch中去执行。Catch中的语句就是对这类异常的处理过程。
既然异常是一种帮助我们编写健壮程序的良好机制,那么我们的很多模块一定就具备一些抛出异常的代码,用来通知应用模块某种错误或者情况的发生,以便我们的应用代码对这些情况做出相应的处理。而这种异常抛出,也是我们需要进行测试的一个方面。可以想见,一个本该抛出的异常,因为某种原因,没有抛出或者抛出了错误的异常,就会导致上层应用作出错误的处理。轻则影响软件的某些局部功能,严重的甚至会直接让整个软件崩溃,这种例子在软件开发中是屡见不鲜的。
NUnit也提供了很多办法让我们在测试中添加针对异常的测试,包括一些属性和断言。 我们现在来了解下NUnit用于测试异常的一个属性。
[ExpectedException( "System.ArgumentException", ExpectedMessage="expected message",UserMessage ="custom message" )]
这个属性加在定义测试的函数上,用来告诉NUnit,这个测试要求抛出一个指定类型的异常,并且异常的Message属性与ExpectedMessage定义的一致。如果没有在测试中捕获这类异常,就测试失败,并输出UserMessage定义的信息作为错误信息。按照惯例,我们将在实例中学习到如何使用这个属性来测试异常。
为了说明问题,我们设计了一个叫做Garage的模块,这个模块模拟了一个容纳5辆车的车库,提供2个方法来实现入库、出库功能(CheckIn和CheckOut)。同时提供了一个管理的功能,即开启/关闭车库(Open和Close,我们的这个车库不是24小时营业),见如下对象接口图:
整个车库模块提供了2大类异常
GarageException:这个异常主要用来体现车库本身的功能性错误,每种错误通过异常的Message属性来区分。比如“Garage Closed”表示车库操作时候车库不在营业状态的错误,“Garage Full”表示车库已满时候使用了CheckIn功能导致的错误。
privilegeException:这个异常用来体现对车库的非常管理操作。车库的Open和Close操作都需要提供密码,如果密码不对,就会有这样的异常发生。
整个模块对于异常的设计如下:
1. 如果在密码错误的情况下对车库进行管理操作(Open和Close),需要抛出privilegeException,Message为“No Permit Garage Management”。
2. 如果在车库关闭的情况下,进行出库/入库操作(CheckIn和CheckOut),需要抛出GarageException,Message为“Garage Closed”。
3. 如果在车库已满的情况下(库中车辆满5辆),进行入库操作(CheckIn),需要抛出GarageException,Message为“Garage Full”。
4. 如果在车库已空的情况下,进行出库操作(CheckOut),需要抛出GarageException,Message为”Garage Empty“。
整个功能模块代码如下:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace MyGarageNS
{
public class GarageException : Exception
{
public GarageException(string message)
: base(message)
{
}
}
public class privilegeException : Exception
{
public privilegeException( )
: base("No Permit Garage Management")
{
}
}
public class MyGarage
{
private List carQueue = new List();
private bool _isClosed = false;
public void CheckIn(string carSN)
{
if (_isClosed == true)
{
throw new GarageException("Garage Closed");
}
if (carQueue.Count >= 5)
{
throw new GarageException("Garage Full");
}
else
{
carQueue.Add(carSN);
}
}
public void CheckOut(string carSN)
{
if (_isClosed)
{
throw new GarageException("Garage Closed");
}
if (carQueue.Count == 0)
{
throw new GarageException("Garage Empty");
}
else
{
carQueue.Remove(carSN);
}
}
public void Open(string password)
{
if (password == "garage")
{
_isClosed = false;
}
else
{
throw new privilegeException();
}
}
public void Close(string password)
{
if (password == "garage")
{
_isClosed = true;
}
else
{
throw new privilegeException();
}
}
}
}
现在我们针对该模块的异常设计,来设计我们的测试(白盒测试应该是针对设计的)。
1. 设计针对Open和Close提供密码错误时的异常测试,这里我们定义应该抛出的异常为privilegeException,异常的Message属性为”No Permit Garage Management“,如果没有抛出这样的异常,则测试失败,并且告诉测试人员"Expected PrivilegeException does not throw from Garage Class as expected"。代码如下
[Test]
[ExpectedException(typeof(privilegeException), ExpectedMessage = "No Permit Garage Management", UserMessage = "Expected PrivilegeException does not throw from Garage Class as expected")]
public void OpenPrivilegeTest()
{
MyGarage garage = new MyGarage();
garage.Open("key");
}
[Test]
[ExpectedException(typeof(privilegeException), ExpectedMessage = "No Permit Garage Management", UserMessage = "Expected PrivilegeException does not throw from Garage Class as expected")]
public void ClosePrivilegeTest()
{
MyGarage garage = new MyGarage();
garage.Close("key");
}
2. 针对车库关闭的情况下CheckIn和CheckOut的异常测试,这里我们定义应该抛出GarageException类型的异常,异常的Message属性为"Garage Closed",不然测试失败,并输出错误信息"Expected GarageException does not throw when checking in closed garage"。初学者在这里容易犯的错误是看需要的异常类型和Message一样,就把2个测试放在了一个Test中间,这样的话,只要CheckIn或者CheckOut2个功能有一个可以按照设计抛出异常,测试就会通过了,这样的测试是不完整的。代码如下,大家可以注意下,这次我们没用有typeof的方式来获取异常类型作为ExpectedException的参数定义期望异常,而是直接使用string方式来识别异常,这样做的好处是无需在测试工程中添加异常类型定义所在的组件的引用,这在某些时候是很方便的。
[Test]
[ExpectedException("MyGarageNS.GarageException", ExpectedMessage = "Garage Closed", UserMessage = "Expected GarageException does not throw when checking in closed garage")]
public void GarageCloseTest1()
{
MyGarage garage = new MyGarage();
garage.CheckIn("myCar");
garage.Close("garage");
garage.CheckIn("myCar2");
}
[Test]
[ExpectedException("MyGarageNS.GarageException", ExpectedMessage = "Garage Closed", UserMessage = "Expected GarageException does not throw when checking out closed garage")]
public void GarageCloseTest2()
{
MyGarage garage = new MyGarage();
garage.CheckIn("myCar");
garage.Close("garage");
garage.CheckOut("myCar");
}
[Test]
[ExpectedException("MyGarageNS.GarageException", ExpectedMessage = "Garage Full", UserMessage = "Expected GarageException does not throw when checking in full garage")]
public void GarageFullTest()
{
MyGarage garage = new MyGarage();
for (int i = 0; i < 5; i++)
{
garage.CheckIn("car" + i.ToString());
}
garage.CheckIn("myCar");
}
4. 在车库为空的情况下,执行CheckOut功能的异常测试,如下,我们定义需要抛出的异常为GarageException类型,异常的Message属性为"Garage Empty"。否则测试失败,并输出错误信息"Expected GarageException does not throw when checking out empty garage"。代码如下
[Test]
[ExpectedException("MyGarageNS.GarageException", ExpectedMessage = "Garage Empty", UserMessage = "Expected GarageException does not throw when checking out empty garage")]
public void GarageEmptyTest()
{
MyGarage garage = new MyGarage();
garage.CheckOut("myCar");
}
编译测试,并执行,查看结果如下
从上面结果可以看出,我们的模块设计的还可以,所有的异常情况都按照设计编写好了。有了这些异常,上层模块就可以捕获他们来了解问题所在,并正确处理了。如果我们的异常设计有问题了,我们的测试代码会发现么?让我们来看下。
我们修改2个功能如下
1. CheckOut:我们修改该功能,让它在车库空的情况下,不抛出任何异常,而且继续出库。这里我们仅仅简单的注释掉抛出异常的语句。
public void CheckOut(string carSN)
{
if (_isClosed)
{
throw new GarageException("Garage Closed");
}
if (carQueue.Count == 0)
{
//throw new GarageException("Garage Empty");
}
else
{
carQueue.Remove(carSN);
}
}
public void Close(string password)
{
if (password == "garage")
{
_isClosed = true;
}
else
{
throw new Exception();
}
}
现在我们重新编译模块代码,不需要重新编译测试。然后Run一次看下结果如何。
可以看到,我们修改过的2个功能的异常测试失败了,来分析下测试失败的输出。
1. MyGarageTest.GarageTest.ClosePrivilegeTest: 这个测试错误信息的紫色部分是我们定义于测试中的失败信息。通过它我们知道测试失败的原因。
蓝色部分告诉我们,该测试还是抛出了一个异常,但是这个异常不是我们期望的异常。也就是说,我们的测试是正确的区分了不同异常的(这里抛出的是Exception这个通用异常)。红色部分类似以前我们看到的,期望值和实际值的比较。通过这样的比较,我们可以很快了解设计错误的地方。
Expected PrivilegeException does not throw from Garage Class as expected
An unexpected exception type was thrown
Expected: MyGarageNS.privilegeException
but was: System.Exception : Exception of type 'System.Exception' was thrown.
2. MyGarageTest.GarageTest.GarageEmptyTest: 这个错误信息的紫色部分依然是我们定义于测试中的失败信息。而红色部分是NUnit的错误信息,告诉我们期望得到的异常。因为没有其他异常抛出,也就没有实际获取的异常提示了。
Expected GarageException does not throw when checking out empty garage
MyGarageNS.GarageException was expected
现在我们已经学会如何在NUnit中通过ExpectedException属性来测试程序的异常,同时也看到了,异常是一个设计良好的模块必不可少的机制,可以帮助我们构建健壮的程序。
NUnit提供给我们一种方式来模糊匹配所要测试的异常Message属性,通过这种方式,我们无需完全给出Message的全部信息,而只需给出某种匹配条件。我们修改GarageFullTest来看下这种方法
[Test]
[ExpectedException("MyGarageNS.GarageException", ExpectedMessage = "Full",MatchType = MessageMatch.Contains, UserMessage = "Expected GarageException does not throw when checking in full garage")]
public void GarageFullTest()
{
MyGarage garage = new MyGarage();
for (int i = 0; i < 5; i++)
{
garage.CheckIn("car" + i.ToString());
}
garage.CheckIn("myCar");
}
这样,只要CheckIn功能抛出的GarageException的Message属性包含"Full"这个字串,那么就该测试就能通过。我们跑下该测试,看下结果
MatchType可以有3种方式
MessageMatch.Contains: 只要异常Message中包含ExpectedMessage定义的字串即匹配。
MessageMatch.StartsWith:只要异常Message以ExpectedMessage定义的字串开头即匹配。
MessageMatch.Exact: 需要异常Message和ExpectedMessage定义的字串精确匹配。
MessageMatch.Regex:ExpectedMessage定义的是一个正则表达式(关于正则表达式大家可以上网查找相关资料,也可以看我以后的专题),用于定义Message匹配的模板。
灵活运用MatchType,可以完成很多巧妙的测试,以后我们会带大家体会到。
到目前为止我们在ExpectedException中只定义了测试失败时的错误信息输出,而无法实现更多的功能。但是在一个完整的白盒测试框架中,Log系统是必不可少的,而Log系统的日志都是在测试中输出的。比如我们想在一个异常测试失败的时候,将失败的原因甚至调用栈信息都输出到测试Log中,单单依靠前面的方法已经无法满足需求了。NUnit提供了一个非常有用的办法,让我们在异常测试中调用自己的代码,来完成更为高级的功能,就是ExpectedException的Handler参数。通过它,我们可以指定一个函数,该函数在指定异常产生时被调用,来完成进一步的功能。
修改GarageCloseTest2的测试,使期望异常产生时,弹出一个MessageBox来告诉我们调用栈的情况。
[Test]
[ExpectedException("MyGarageNS.GarageException", Handler = "HandlerMethod")]
public void GarageCloseTest2()
{
MyGarage garage = new MyGarage();
garage.CheckIn("myCar");
garage.Close("garage");
garage.CheckOut("myCar");
}
public void HandlerMethod(Exception ex)
{
MessageBox.Show(ex.StackTrace);
}
这段代码中的Hander指向了HandlerMethod参数,一旦测试抛出了GarageException异常,就会自动调用我们定义的HandlerMethod函数。现在来看下运行结果是不是符合我们的预期。
我们点掉这个MessageBox,可以看到测试继续执行直到完成,结果如下
如果这个测试没有捕获到预期的异常,结果将会是如下,并且不会调用我们定义的处理函数,也就不会弹出MessageBox
至此,我们算是初步了解了异常以及NUnit测试中的异常测试方法,当然这并不代表我们已经了解了NUnit异常处理的全部,恰恰相反,我们现在所见的,只是一些皮毛。NUnit有针对异常测试的一组断言,我们将会在今后的学习中接触到。本节的内容稍稍有点多,希望我的讲解足够清晰,不至于给大家带来困扰。另外,本节的Garage类中,我留了一个小小的BUG,需要大家设计新的测试,来发现这个BUG并合理的利用本节的知识对他进行处理。算是我给大家的一个小小悬念吧,想到如何设计该测试的朋友可以直接留言并附上你的用例,希望不久,我们就会看到有高手出现^_^。
最后还是那句老话,请继续关注本系列课程,如果对课程有不理解的问题及好的建议,可以发邮件给我[email protected],谢谢。
Rss订阅IQuickTest(关于如何订阅?)