【Nunit入门系列讲座 4】NUnit断言- 对象识别断言
上节我们初步认识了NUnit断言的作用以及如何在测试中通过断言构成测试。其实NUnit的断言系统功能庞大而丰富,不是简简单单一篇教程就能尽述的,在后续的我们会一点点深入学习,一旦掌握了NUnit的断言系统,也就可以说是入门了。一些朋友会发现,上节介绍的数值相等/不等断言,并不能很好地适应现在软件测试的需求。在过去的面向过程软件中,没有类和对象的概念,数值类断言基本可以胜任。但是现代软件设计中,面向对象已经是一种非常普遍的设计方法,在不同功能模块之间传递的不再是单纯的数值参数,很多时候,对象才是模块之间信息交互的载体。本节中我们就来了解一组适合于对象的断言,以及体会下这类断言在面向对象软件测试中是如何应用的。
一、背景知识:类和对象
相信大家一定经常听到这2个概念,这是面向对象软件的基本概念。这个概念很多书都有介绍,有些介绍的很清楚,有些介绍的不清不楚。其实这个理解可深可浅,我在这里简单解释下,关于这2个概念的理解和领会,需要大家在使用的过程中,逐渐加深。如果用一个比喻来描述类和对象的话,类就如同生物界的DNA编码,不同种类的生物,有不同的DNA编码,同一类的生物,有着相同的DNA编码(我们不考虑基因突变和一些个体的差异)。而对象就是具体的一个生物个体,他的特征是由DNA编码定义好的。比如人这个种类(class),每个人(instance)都具有相同的DNA编码,这就决定了作为人,都是有人的一些特征的,不具备这些特征,那也就不属于人类这个类别的了。回到软件来看,我们定义一个类,就是如同定义了一段DNA编码,而new这个类的一个对象,就如同根据DNA孵化一个真实的动物(也包括人类)。这样,我们就可以理解到类是对象的模板,而对象是类的实例这个说法的意思了。
换个视角,从程序本身来考虑,类定义是保存在程序文件中的一段代码,这个在程序没有运行的时候就是存在的。而对象是根据这段代码以及程序的对象构造语句,在内存中创建的一段空间,这段空间保存了这个对象相关的所有信息,包括属性值,以及成员函数。也就是说,程序本体中保存了类定义,而只有程序运行的时候,对象才会被生成,没有运行的程序是没有对象的,对象是存在于内存中的。可以从下图体会下这种布局:
二、对象识别断言(Identity Asserts)及其应用
在面向对象的白盒测试中,NUnit提供了针对对象的一组断言,我们来了解一下它们。
Assert.AreSame( object expected, object actual );
Assert.AreSame( object expected, object actual, string message );
Assert.AreSame( object expected, object actual, string message,
params object[] parms );
Assert.AreNotSame( object expected, object actual );
Assert.AreNotSame( object expected, object actual, string message );
Assert.AreNotSame( object expected, object actual, string message,
params object[] parms );
Assert.Contains( object anObject, IList collection );
Assert.Contains( object anObject, IList collection,
string message );
Assert.Contains( object anObject, IList collection,
string message, params object[] parms );
Assert.AreSame用来判断2个对象是否相同。这里要特别注意,2个对象相等,并不是指2个对象的属性完全相同即为相等。而是指2个对象指向内存中同一区域,即2个变量expect和actual指向同一个内存中的实例。结合上面的图,可以看到其实a,b2个对象内容完全相同,但是他们是内存中的2个不同实例,也就是2个不同对象。这样的2个对象不能称为相等。只有像a,c那样,指向内存中的同一实例,才可以成为a,c2个对象相同。
Assert.AreNotSame用来判断2个对象是否是不同对象,也就是指向不同的内存实例。即使一个对象是有另外一个对象拷贝过来的,只要他们是在内存中的不同区域,也就是不等的。
Assert.Contains是一个很有用的断言。他判断一个对象是否在一个已知的对象队列中。实际测试中,我们往往关心的不是2个对象是否相等,而是关心应该生成或者传递的对象是否成功生成以及传递了,这个时候,这样的断言极大的提高了我们的判定效率,这点我们将会在后面的例子中体会到。
了解了这3组断言的含义后,让我们在一个实际测试的过程中,来学习和理解他们的使用。个人一直觉得泛泛的解释断言系统将会枯燥而不易掌握,所以我们通常会通过一个实际模块的测试,在应用中带大家学习新断言的用法并体会一些软件设计和测试中的思想及技巧。
这次我们将会测试一个模拟汽车生产工厂的软件模块,代码如下
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace FactoryNS
{
public enum color
{
Red = 0,
White,
Gray,
Unknown
}
public class Car
{
public color color;
public int price = 0;
public virtual void Run()
{
Console.WriteLine("could not run");
}
public virtual void Stop()
{
Console.WriteLine("no need to stop");
}
}
public class RedCar:Car
{
public RedCar(int price)
{
this.color = color.Red;
this.price = price;
}
public RedCar()
{
this.color = color.Red;
this.price = 120;
}
public override void Run()
{
Console.WriteLine("Red Car start to run");
}
public override void Stop()
{
Console.WriteLine("Red Car stop");
}
}
public class WhiteCar:Car
{
public WhiteCar()
{
this.color = color.White;
this.price = 100;
}
public WhiteCar(int price)
{
this.color = color.White;
this.price = price;
}
public override void Run()
{
Console.WriteLine("White Car start to run");
}
public override void Stop()
{
Console.WriteLine("White Car stop");
}
}
public class GrayCar:Car
{
public GrayCar()
{
this.color = color.Gray;
this.price = 90;
}
public GrayCar(int price)
{
this.color = color.Gray;
this.price = price;
}
public override void Run()
{
Console.WriteLine("Gray Car start to run");
}
public override void Stop()
{
Console.WriteLine("Gray Car stop");
}
}
// factory to produce cars according to orders
public class CarFactory
{
public List<Car> wareHouse;
public Car lastCar;
// order procedure by color
public void order(color color)
{
switch (color)
{
case color.Red:
lastCar = new RedCar();
this.wareHouse.Add(lastCar);
break;
case color.White:
lastCar = new WhiteCar();
this.wareHouse.Add(lastCar);
break;
case color.Gray:
lastCar = new GrayCar();
this.wareHouse.Add(lastCar);
break;
default:
break;
}
}
}
}
CarFactory 就是一个轿车生产的工厂类,他提供给客户3种类型的轿车-红色,白色及灰色。每种类型车辆的价格不同(所以不能提供给客户错误的种类,不然问题就大了)。客户可以通过CarFactory的order方法来下订单,并告诉工厂所需要的汽车类型。然后通过工厂类的lastCar属性,可以拿到自己的汽车。而工厂接受到订单(order方法被调用)后,就会根据客户的需求来生产车辆,并放到仓库中(wareHouse属性)。每辆车都有Run和Stop方法,客户拿到车(通过lastCar属性)后,就可以开开停停,试试车子是不是好的,或者是不是自己想要的(不同颜色车子的Run和Stop会输出不同信息)。
我们来想下,针对这个模块,我们应该做些什么测试。首先,要保证客户都能订到自己想要的车,并且不能出现几个客户订到了同一部车的情况(一房多卖的事情我们还是不干的)。那么我们就通过这样的方法来做这个测试:我们模拟2个客户订2部同样颜色的车子,然后比较这2部车是不是同一部车。可以通过如下的测试代码来完成
//same color cars ordering would work correctly
[Test]
public void SameColorOrderTest()
{
factory.order(color.Red);
Car myCar1 = factory.lastCar;
factory.order(color.Red);
Car myCar2 = factory.lastCar;
Assert.AreNotSame(myCar1, myCar2);
}
这里我们就用了Assert.AreNotSame断言来判断我们订的2辆车是不是同一部,2辆车没有任何区别,颜色,价格(对象属性都相等),但是他们不应该是同一部车(对象不应该相等)。
其次,我们还想测试一下这个工厂的意外情况处理能力,在程序中就是是否针对某些特殊情况作了考虑,这个也是一个程序是否优秀的标志之一。一个考虑周全的程序和一个考虑不周全的程序,在面对复杂的用户的时候,可能就是质的差别。对于这个汽车工厂来说,有种情况就是客户只下了订单,但是忘记告诉我们颜色了。我们希望这种时候,CarFactory能够生产给客户灰色的车辆,因为他比较大众些。大家可能已经注意到了,在CarFactory的模块设计中并没有很好的考虑到这种情况。我们下面就来设计这样的测试,并看最终是否能通过测试发现这里设计上的不足。这次我们模拟客户,先订一辆白色的,然后再下一个没有颜色的订单,看最终CarFactory我们的是什么样的车。理论上,他应该也给我们一辆不同于前一个订单的车,并且是我们希望的灰色。测试代码如下:
//order with no color designated will be accepted correctly
[Test]
public void NoColorOrderTest()
{
factory.order(color.White);
Car myCar1 = factory.lastCar;
factory.order(color.Unknown);
Car myCar2 = factory.lastCar;
Assert.AreNotSame(myCar1, myCar2);
Assert.AreEqual(color.Gray, myCar2.color);
}
下面我们来看下最终的测试结果
可以看到,第一个同样色车辆订单的测试通过了,CarFactory确实给我们生产了不同的车辆。而第二个未注明颜色订单的测试失败了,为什么失败了呢?我们来看下错误信息告诉了我们些什么。
CarFactoryTestSet.CarFactoryTest.NoColorOrderTest:
Expected: not same as <FactoryNS.WhiteCar>
But was: <FactoryNS.WhiteCar>
我们可以看到,原本我们期望得到的是一辆不同于前一订单的灰色新车,但是实际上,CarFactory并没有为我们生产这辆车。所以我们通过lastCar拿到的还是前面生产的那辆白色轿车。更改CarFactory的order函数如下
public void order(color color)
{
switch (color)
{
case color.Red:
lastCar = new RedCar();
this.wareHouse.Add(lastCar);
break;
case color.White:
lastCar = new WhiteCar();
this.wareHouse.Add(lastCar);
break;
case color.Gray:
lastCar = new GrayCar();
this.wareHouse.Add(lastCar);
break;
default:
lastCar = new GrayCar();
this.wareHouse.Add(lastCar);
break;
}
}
重新编译CarFactory模块,重新运行测试看下结果。
至此,我们通过对象识别断言,完成了对CarFactory模块主要功能的测试。但是等等,CarFactory还有一个功能,就是生产完了车辆后会将车放入仓库(wareHouse)中。下面我们就来针对这个功能,编写测试。我们生产完几辆车后,来检查一下,前面生产的车,是不是都放在了仓库中。对了,很多朋友一定也想到了,我们要用Assert.Contains这个断言来完成这样的功能。测试代码如下:
//check if all cars ordered are in warehouse
[Test]
public void warehouseTest()
{
factory.order(color.White);
Car myCar1 = factory.lastCar;
factory.order(color.Red);
Car myCar2 = factory.lastCar;
factory.order(color.Gray);
Car myCar3 = factory.lastCar;
Assert.Contains(myCar1, factory.wareHouse);
Assert.Contains(myCar2, factory.wareHouse);
Assert.Contains(myCar3, factory.wareHouse);
}
然后运行该测试,看下结果如何
可以看到,这个功能是通过测试的,如果我们故意把灰色车辆的入库功能写错,测试会如何呢,将CarFactory中灰色车辆订单的处理改成如下
case color.Gray:
lastCar = new GrayCar();
break;
重新编译功能模块,然后运行测试
从测试的错误信息中,我们可以看出,入库的只有红色和白色的车辆。
三、结语
到这里我们已经掌握了NUnit中的对象识别断言,希望大家能从中体会到NUnit断言的强大及面向对象环境下白盒测试的新的要求和特点。还是那句老话,如果觉得不错,请继续关注本系列课程。
【Nunit入门系列讲座 5】Nunit如何测试程序中的异常 —— 初识异常及异常测试
在了解了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也提供了很多办法让我们在测试中添加针对异常的测试,包括一些属性和断言。 我们现在来了解下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<string> carQueue = new List<string>();
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");
}
3. 在车库已满的情况下,执行CheckIn功能的异常测试,我们定义需要抛出的异常为GarageException类型,异常的Message属性为"Garage Full"。否则测试失败,并输出错误信息"Expected GarageException does not throw when checking in full garage",代码如下
[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);
}
}
2. Close: 我们修改该功能,让他在密码不对的情况下,抛出一个通用的异常Exception。
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属性来测试程序的异常,同时也看到了,异常是一个设计良好的模块必不可少的机制,可以帮助我们构建健壮的程序。
三、ExpectedException的一些补充
1. 对异常的Message属性的模糊匹配
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,可以完成很多巧妙的测试,以后我们会带大家体会到。
2. 添加异常处理函数来扩展我们的测试
到目前为止我们在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并合理的利用本节的知识对他进行处理。算是我给大家的一个小小悬念吧,想到如何设计该测试的朋友可以直接留言并附上你的用例,希望不久,我们就会看到有高手出现^_^。
|