热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

【Nunit入门系列讲座5】Nunit如何测试程序中的异常——初识异常及异常测试

作者:shinoy时间:20111121版权所有,侵权必究。出处:http:blog.csdn.netsnowshinoy本节示例代码下载:示例代码
作者:shinoy
时间:2011/11/21 版权所有,侵权必究。
出处: http://blog.csdn.net/snowshinoy


本节示例代码下载: 示例代码

        在了解了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 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");
}

       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并合理的利用本节的知识对他进行处理。算是我给大家的一个小小悬念吧,想到如何设计该测试的朋友可以直接留言并附上你的用例,希望不久,我们就会看到有高手出现^_^。

        最后还是那句老话,请继续关注本系列课程,如果对课程有不理解的问题及好的建议,可以发邮件给我28345697@qq.com,谢谢。


白盒测试QQ交流群:点击这里加入此群

 Rss订阅IQuickTest关于如何订阅?

GoogleReader订阅地址: http://feeds.feedburner.com/iquicktest


推荐阅读
  • Java容器中的compareto方法排序原理解析
    本文从源码解析Java容器中的compareto方法的排序原理,讲解了在使用数组存储数据时的限制以及存储效率的问题。同时提到了Redis的五大数据结构和list、set等知识点,回忆了作者大学时代的Java学习经历。文章以作者做的思维导图作为目录,展示了整个讲解过程。 ... [详细]
  • Android JSON基础,音视频开发进阶指南目录
    Array里面的对象数据是有序的,json字符串最外层是方括号的,方括号:[]解析jsonArray代码try{json字符串最外层是 ... [详细]
  • 本文介绍了在Vue项目中如何结合Element UI解决连续上传多张图片及图片编辑的问题。作者强调了在编码前要明确需求和所需要的结果,并详细描述了自己的代码实现过程。 ... [详细]
  • 本文介绍了Python爬虫技术基础篇面向对象高级编程(中)中的多重继承概念。通过继承,子类可以扩展父类的功能。文章以动物类层次的设计为例,讨论了按照不同分类方式设计类层次的复杂性和多重继承的优势。最后给出了哺乳动物和鸟类的设计示例,以及能跑、能飞、宠物类和非宠物类的增加对类数量的影响。 ... [详细]
  • 模板引擎StringTemplate的使用方法和特点
    本文介绍了模板引擎StringTemplate的使用方法和特点,包括强制Model和View的分离、Lazy-Evaluation、Recursive enable等。同时,还介绍了StringTemplate语法中的属性和普通字符的使用方法,并提供了向模板填充属性的示例代码。 ... [详细]
  • 欢乐的票圈重构之旅——RecyclerView的头尾布局增加
    项目重构的Git地址:https:github.comrazerdpFriendCircletreemain-dev项目同步更新的文集:http:www.jianshu.comno ... [详细]
  • PHP图片截取方法及应用实例
    本文介绍了使用PHP动态切割JPEG图片的方法,并提供了应用实例,包括截取视频图、提取文章内容中的图片地址、裁切图片等问题。详细介绍了相关的PHP函数和参数的使用,以及图片切割的具体步骤。同时,还提供了一些注意事项和优化建议。通过本文的学习,读者可以掌握PHP图片截取的技巧,实现自己的需求。 ... [详细]
  • CSS3选择器的使用方法详解,提高Web开发效率和精准度
    本文详细介绍了CSS3新增的选择器方法,包括属性选择器的使用。通过CSS3选择器,可以提高Web开发的效率和精准度,使得查找元素更加方便和快捷。同时,本文还对属性选择器的各种用法进行了详细解释,并给出了相应的代码示例。通过学习本文,读者可以更好地掌握CSS3选择器的使用方法,提升自己的Web开发能力。 ... [详细]
  • 本文探讨了C语言中指针的应用与价值,指针在C语言中具有灵活性和可变性,通过指针可以操作系统内存和控制外部I/O端口。文章介绍了指针变量和指针的指向变量的含义和用法,以及判断变量数据类型和指向变量或成员变量的类型的方法。还讨论了指针访问数组元素和下标法数组元素的等价关系,以及指针作为函数参数可以改变主调函数变量的值的特点。此外,文章还提到了指针在动态存储分配、链表创建和相关操作中的应用,以及类成员指针与外部变量的区分方法。通过本文的阐述,读者可以更好地理解和应用C语言中的指针。 ... [详细]
  • 本文讨论了如何在dotnet桌面(Windows)应用程序中添加图标。作者提到可以使用dotnet命令行工具与resource.rc文件一起使用来为标准.NET核心应用程序添加图标。作者还介绍了在创建控制台应用程序时如何编辑projeto1.csproj文件来添加图标。 ... [详细]
  • 本文介绍了深入浅出Linux设备驱动编程的重要性,以及两种加载和删除Linux内核模块的方法。通过一个内核模块的例子,展示了模块的编译和加载过程,并讨论了模块对内核大小的控制。深入理解Linux设备驱动编程对于开发者来说非常重要。 ... [详细]
  • IjustinheritedsomewebpageswhichusesMooTools.IneverusedMooTools.NowIneedtoaddsomef ... [详细]
  • 本文介绍了在处理不规则数据时如何使用Python自动提取文本中的时间日期,包括使用dateutil.parser模块统一日期字符串格式和使用datefinder模块提取日期。同时,还介绍了一段使用正则表达式的代码,可以支持中文日期和一些特殊的时间识别,例如'2012年12月12日'、'3小时前'、'在2012/12/13哈哈'等。 ... [详细]
  • 本文介绍了获取关联数组键的列表的方法,即使用Object.keys()函数。同时还提到了该方法在不同浏览器的支持情况,并附上了一个代码片段供读者参考。 ... [详细]
  • 本文介绍了使用哈夫曼树实现文件压缩和解压的方法。首先对数据结构课程设计中的代码进行了分析,包括使用时间调用、常量定义和统计文件中各个字符时相关的结构体。然后讨论了哈夫曼树的实现原理和算法。最后介绍了文件压缩和解压的具体步骤,包括字符统计、构建哈夫曼树、生成编码表、编码和解码过程。通过实例演示了文件压缩和解压的效果。本文的内容对于理解哈夫曼树的实现原理和应用具有一定的参考价值。 ... [详细]
author-avatar
springzhe7943
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有