.NET单元测试学习(三)--Using NUnit

NUnit是.net平台上使用得最为广泛的测试框架之一,本文将通过示例来描述NUnit的使用方法,并提供若干编写单元测试的建议和技巧,供单元测试的初学者参考。

继续下文之前,先来看看一个非常简单的测试用例(TestCase):

1  [Test]
2  public   void  AdditionTest()
3  {
4       int  expectedResult  =   2 ;
5 
6      Assert.AreEqual(exptectedResult,  1   +   1 );
7  }

你肯定会说这个TestCase也太白痴了吧!这也是许多NUnit文档被人诟病的一点,但是我的理解并不是这样,xUnit本来就是编写UT的简易框架,keep it simple and stupid,任何通过复杂的TestCase来介绍NUnit的用法都是一种误导,UT复杂之处在于如何在实际项目中应用和实施,而不是徘徊于该如何使用NUnit。

主要内容:
1、NUnit的基本用法
2、测试用例的组织
3、NUnit的断言(Assert)
4、常用单元测试工具介绍

一、NUnit的基本用法
和其他xNUnit框架不同的是,NUnit框架使用Attribute(如前面代码中的[Test])来描述测试用例的,也就是说我们只要掌握了 Attribute的用法,也就基本学会如何使用NUnit了。VSTS所集成的单元测试也支持类似NUnit的Attributes,下表对比了 NUnit和VSTS的标记:

usage

NUnit attributes

VSTS attributes

标识测试类

TestFixture

TestClass

标识测试用例(TestCase

Test

TestMethod

标识测试类初始化函数

TestFixtureSetup

ClassInitialize

标识测试类资源释放函数

TestFixtureTearDown

ClassCleanup

标识测试用例初始化函数

Setup

TestInitialize

标识测试用例资源释放函数

TearDown

TestCleanUp

标识测试用例说明

N/A

Description

标识忽略该测试用例

Ignore

Ignore

标识该用例所期望抛出的异常

ExpectedException

ExpectedException

标识测试用例是否需要显式执行

Explicit

?

标识测试用例的分类

Category

?


现在,让我们找一个场景,通过示例来了解上述NUnit标记的用法。来看看一个存储在数据库中的数字类:
.NET单元测试学习(三)--Using NUnit_第1张图片
这是我们常见的DAL+Entity的设计,DigitDataProvider和Digit类的实现代码如下:

1)Digit.cs类:

 1  using  System;
 2  using  System.Data;
 3 
 4  namespace  Product
 5  {
 6       ///   <summary>
 7       ///  Digit 的摘要说明
 8       ///   </summary>
 9       ///  创 建 人: Aero
10       ///  创建日期: 2006-3-22
11       ///  修 改 人: 
12       ///  修改日期:
13       ///  修改内容:
14       ///  版    本:
15       public   class  Digit
16      {
17           private  Guid _digitId;
18           public  Guid DigitID
19          {
20               get  {  return   this ._digitId; }
21               set  {  this ._digitId  =  value; }
22          }
23 
24           private   int  _value  =   0 ;
25           public   int  Value
26          {
27               get  {  return   this ._value; }
28               set  {  this ._value  =  value; }
29          }
30 
31           #region  构造函数
32           ///   <summary>
33           ///  默认无参构造函数
34           ///   </summary>
35           ///  创 建 人: Aero
36           ///  创建日期: 2006-3-22
37           ///  修 改 人: 
38           ///  修改日期:
39           ///  修改内容:
40           public  Digit()
41          {
42               //
43               //  TODO: 在此处添加构造函数逻辑
44               //
45          }
46 
47           ///   <summary>
48           ///  construct the digit object from a datarow
49           ///   </summary>
50           ///   <param name="row"></param>
51           public  Digit(DataRow row)
52          {
53               if  (row  ==   null )
54              {
55                   throw   new  ArgumentNullException();
56              }
57 
58               if  (row[ " DigitID " !=  DBNull.Value)
59              {
60                   this ._digitId  =   new  Guid(row[ " DigitID " ].ToString());
61              }
62 
63               if  (row[ " Value " !=  DBNull.Value)
64              {
65                   this ._value  =  Convert.ToInt32(row[ " Value " ]);
66              }
67          }
68          
69           #endregion
70      }
71  }
72 


2)DigitDataProvider类:

  1  using  System;
  2  using  System.Data;
  3  using  System.Data.SqlClient;
  4  using  System.Collections;
  5 
  6  namespace  Product
  7  {
  8       ///   <summary>
  9       ///  DigitDataProvider 的摘要说明
 10       ///   </summary>
 11       ///  创 建 人: Aero
 12       ///  创建日期: 2006-3-22
 13       ///  修 改 人: 
 14       ///  修改日期:
 15       ///  修改内容:
 16       ///  版    本:
 17       public   class  DigitDataProvider
 18      {
 19           ///   <summary>
 20           ///  定义数据库连接
 21           ///   </summary>
 22           private  SqlConnection _dbConn;
 23           public  SqlConnection Connection
 24          {
 25               get  {  return   this ._dbConn; }
 26               set  {  this ._dbConn  =  value; }
 27          }
 28          
 29           #region  构造函数
 30           ///   <summary>
 31           ///  默认无参构造函数
 32           ///   </summary>
 33           ///  创 建 人: Aero
 34           ///  创建日期: 2006-3-22
 35           ///  修 改 人: 
 36           ///  修改日期:
 37           ///  修改内容:
 38           public  DigitDataProvider()
 39          {
 40               //
 41               //  TODO: 在此处添加构造函数逻辑
 42               //
 43          }
 44 
 45           public  DigitDataProvider(SqlConnection conn)
 46          {
 47               this ._dbConn  =  conn;
 48          }
 49          
 50           #endregion
 51          
 52           #region  成员函数定义
 53 
 54           ///   <summary>
 55           ///  retrieve all Digits in the database
 56           ///   </summary>
 57           ///   <returns></returns>
 58           public  ArrayList GetAllDigits()
 59          {
 60               //  retrieve all digit record in database
 61              SqlCommand command  =   this ._dbConn.CreateCommand();
 62              command.CommandText  =   " SELECT * FROM digits " ;
 63              SqlDataAdapter adapter  =   new  SqlDataAdapter(command);
 64              DataSet results  =   new  DataSet();
 65              adapter.Fill(results);
 66 
 67               //  convert rows to digits collection
 68              ArrayList digits  =   null ;
 69 
 70               if  (results  !=   null   &&  results.Tables.Count  >   0 )
 71              {
 72                  DataTable table  =  results.Tables[ 0 ];
 73                  digits  =   new  ArrayList(table.Rows.Count);
 74 
 75                   foreach  (DataRow row  in  table.Rows)
 76                  {
 77                      digits.Add( new  Digit(row));
 78                  }
 79              }
 80 
 81               return  digits;
 82          }
 83 
 84           ///   <summary>
 85           ///  remove all digits from the database
 86           ///   </summary>
 87           ///   <returns></returns>
 88           public   int  RemoveAllDigits()
 89          {
 90               //  retrieve all digit record in database
 91              SqlCommand command  =   this ._dbConn.CreateCommand();
 92              command.CommandText  =   " DELETE FROM digits " ;
 93 
 94               return  command.ExecuteNonQuery();
 95          }
 96 
 97           ///   <summary>
 98           ///  retrieve and return the entity of given value
 99           ///   </summary>
100           ///   <exception cref="System.NullReferenceException"> entity not exist in the database </exception>
101           ///   <param name="value"></param>
102           ///   <returns></returns>
103           public  Digit GetDigit( int  value)
104          {
105               //  retrieve entity of given value
106              SqlCommand command  =   this ._dbConn.CreateCommand();
107              command.CommandText  =   " SELECT * FROM digits WHERE Value=' "   +  value  +   " ' " ;
108              SqlDataAdapter adapter  =   new  SqlDataAdapter(command);
109              DataSet results  =   new  DataSet();
110              adapter.Fill(results);
111 
112               //  convert rows to digits collection
113              Digit digit  =   null ;
114 
115               if  (results  !=   null   &&  results.Tables.Count  >   0
116                   &&  results.Tables[ 0 ].Rows.Count  >   0 )
117              {
118                  digit  =   new  Digit(results.Tables[ 0 ].Rows[ 0 ]);
119              }
120               else
121              {
122                   throw   new  NullReferenceException( " not exists entity of given value " );
123              }
124 
125               return  digit;
126          }
127 
128           ///   <summary>
129           ///  remove prime digits from database
130           ///   </summary>
131           ///   <returns></returns>
132           public   int  RemovePrimeDigits()
133          {
134               throw   new  NotImplementedException();
135          }
136 
137           #endregion
138      }
139  }
140 


3)新建测试数据库:

CREATE   TABLE   [ dbo ] . [ digits ]  (
    
[ DigitID ]   [ uniqueidentifier ]   NOT   NULL  ,
    
[ Value ]   [ int ]   NOT   NULL  
ON   [ PRIMARY ]
GO


下面,我们开始尝试为DigitDataProvider类编写UT,新建DigitDataProviderTest.cs类。
1、添加nunit.framework引用
.NET单元测试学习(三)--Using NUnit_第2张图片
并在DigitDataProviderTest.cs中添加:

1  using  NUnit.Framework;


2、编写测试用例
1)标识测试类:NUnit要求每个测试类都必须添加TestFixture的Attribute,并且携带一个public无参构造函数。

1  [TestFixture]
2  public   class  DigitProviderTest
3  {
4       public  DigitProviderTest()
5      {
6      }
7  }


2)编写DigitDataProvider.GetAllDigits()的测试函数

 1  ///   <summary>
 2  ///  regular test of DigitDataProvider.GetAllDigits()
 3  ///   </summary>
 4  [Test]
 5  public   void  TestGetAllDigits()
 6  {
 7       //  initialize connection to the database
 8       //  note: change connection string to ur env
 9      IDbConnection conn  =   new  SqlConnection(
10           " Data source=localhost;user id=sa;password=sa;database=utdemo " );
11      conn.Open();
12 
13       //  preparing test data
14      IDbCommand command  =  conn.CreateCommand();
15       string  commadTextFormat  =   " INSERT INTO digits(DigitID, Value) VALUES('{0}', '{1}') " ;
16 
17       for  ( int  i  =   1 ; i  <=   100 ; i ++ )
18      {
19          command.CommandText  =   string .Format(
20              commadTextFormat, Guid.NewGuid().ToString(), i.ToString());
21          command.ExecuteNonQuery();
22      }
23 
24       //  test DigitDataProvider.GetAllDigits()
25       int  expectedCount  =   100 ;
26      DigitDataProvider provider  =   new  DigitDataProvider(conn  as  SqlConnection);
27      IList results  =  provider.GetAllDigits();
28 
29       //  that works?
30      Assert.IsNotNull(results);
31      Assert.AreEqual(expectedCount, results.Count);
32 
33       //  delete test data
34      command  =  conn.CreateCommand();
35      command.CommandText  =   " DELETE FROM digits " ;
36      command.ExecuteNonQuery();
37 
38       //  close connection to the database
39      conn.Close();
40  }


什么?很丑?很麻烦?这个问题稍后再讨论,先来看看一个完整的测试用例该如何定义:

 1  [Test]
 2  public   void  TestCase()
 3  {
 4       //  1) initialize test environement, like database connection
 5      
 6 
 7       //  2) prepare test data, if neccessary
 8      
 9 
10       //  3) test the production code by using assertion or Mocks.
11      
12 
13       //  4) clear test data
14      
15 
16       //  5) reset the environment
17      
18  }

NUnit要求每一个测试函数都可以独立运行(往往有人会误解NUnit并按照Consoler中的排序来执行),这就要求我们在调用目标函数之前先要初始化目标函数执行所需要的环境,如打开数据库连接、添加测试数据等。为了不影响其他的测试函数,在调用完目标函数后,该测试函数还要负责还原初始环境,如删除测试数据和关闭数据库连接等。对于同一测试类里的测试函数来说,这些操作往往是相同的,让我们对上面的代码进行一次Refactoring, Extract Method:

 1  ///   <summary>
 2  ///  connection to database
 3  ///   </summary>
 4  private   static  IDbConnection _conn;
 5 
 6  ///   <summary>
 7  ///  初始化测试类所需资源
 8  ///   </summary>
 9  [TestFixtureSetUp]
10  public   void  ClassInitialize()
11  {
12       //  note: change connection string to ur env
13      DigitProviderTest._conn  =   new  SqlConnection(
14           " Data source=localhost;user id=sa;password=sa;database=utdemo " );
15      DigitProviderTest._conn.Open();
16  }
17 
18  ///   <summary>
19  ///  释放测试类所占用资源
20  ///   </summary>
21  [TestFixtureTearDown]
22  public   void  ClassCleanUp()
23  {
24      DigitProviderTest._conn.Close();
25  }
26 
27  ///   <summary>
28  ///  初始化测试函数所需资源
29  ///   </summary>
30  [SetUp]
31  public   void  TestInitialize()
32  {
33       //  add some test data
34      IDbCommand command  =  DigitProviderTest._conn.CreateCommand();
35       string  commadTextFormat  =   " INSERT INTO digits(DigitID, Value) VALUES('{0}', '{1}') " ;
36 
37       for  ( int  i  =   1 ; i  <=   100 ; i ++ )
38      {
39          command.CommandText  =   string .Format(
40              commadTextFormat, Guid.NewGuid().ToString(), i.ToString());
41          command.ExecuteNonQuery();
42      }
43  }
44 
45  ///   <summary>
46  ///  释放测试函数所需资源
47  ///   </summary>
48  [TearDown]
49  public   void  TestCleanUp()
50  {
51       //  delete all test data
52      IDbCommand command  =  DigitProviderTest._conn.CreateCommand();
53      command.CommandText  =   " DELETE FROM digits " ;
54 
55      command.ExecuteNonQuery();
56  }
57 
58  ///   <summary>
59  ///  regular test of DigitDataProvider.GetAllDigits()
60  ///   </summary>
61  [Test]
62  public   void  TestGetAllDigits()
63  {
64       int  expectedCount  =   100 ;
65      DigitDataProvider provider  =  
66           new  DigitDataProvider(DigitProviderTest._conn  as  SqlConnection);
67 
68      IList results  =  provider.GetAllDigits();
69       //  that works?
70      Assert.IsNotNull(results);
71      Assert.AreEqual(expectedCount, results.Count);
72  }

NUnit提供了以下Attribute来支持测试函数的初始化:
TestFixtureSetup:在当前测试类中的所有测试函数运行前调用;
TestFixtureTearDown:在当前测试类的所有测试函数运行完毕后调用;
Setup:在当前测试类的每一个测试函数运行前调用;
TearDown:在当前测试类的每一个测试函数运行后调用。

3)编写DigitDataProvider.RemovePrimeDigits()的测试函数
唉,又忘了质数判断的算法,这个函数先不实现(throw new NotImplementedException()),对应的测试函数先忽略。

 1  ///   <summary>
 2  ///  regular test of DigitDataProvider.RemovePrimeDigits
 3  ///   </summary>
 4  [Test, Ignore( " Not Implemented " )]
 5  public   void  TestRemovePrimeDigits()
 6  {
 7      DigitDataProvider provider  =  
 8           new  DigitDataProvider(DigitProviderTest._conn  as  SqlConnection);
 9 
10      provider.RemovePrimeDigits();
11  }

Ignore的用法:

Ignore( string  reason)



4)编写DigitDataProvider.GetDigit()的测试函数
当查找一个不存在的Digit实体时,GetDigit()会不会像我们预期一样抛出NullReferenceExceptioin呢?

 1  ///   <summary>
 2  ///  Exception test of DigitDataProvider.GetDigit()
 3  ///   </summary>
 4  [Test, ExpectedException( typeof (NullReferenceException))]
 5  public   void  TestGetDigit()
 6  {
 7       int  expectedValue  =   999 ;
 8      DigitDataProvider provider  =  
 9           new  DigitDataProvider(DigitProviderTest._conn  as  SqlConnection);
10 
11      Digit digit  =  provider.GetDigit(expectedValue);
12  }

ExpectedException的用法

ExpectedException(Type t)
ExpectedException(Type t, 
string  expectedMessage)



在NUnitConsoler里执行一把,欣赏一下黄绿灯吧。本文相关代码可从UTDemo_Product.rar下载。

二、测试函数的组织
现在有一个性能测试的Testcase,执行一次要花上一个小时,我们并不需要(也无法忍受)每次自动化测试时都去执行这样的Testcase,使用NUnit的Explicit标记可以让这个TestCase只有在显示调用下才会执行:

1  [Test, Explicit]
2  public   void  OneHourTest()
3  {
4      //
5  }


.NET单元测试学习(三)--Using NUnit_第3张图片

不幸的是,这样耗时的TestCase在整个测试工程中可能有数十个,或许更多,我们能不能把这些TestCase都组织起来,要么一起运行,要么不运行呢?NUnit提供的Category标记可实现此功能:

 1  [Test, Explicit, Category( " LongTest " )]
 2  public   void  OneHourTest()
 3  {
 4      ...
 5  }
 6 
 7  [Test, Explicit, Category( " LongTest " )]
 8  public   void  TwoHoursTest()
 9  {
10      ...
11  }

这样,只有当显示选中LongTest分类时,这些TestCase才会执行
.NET单元测试学习(三)--Using NUnit_第4张图片

三、NUnit的断言
NUnit提供了一个断言类NUnit.Framework.Assert,可用来进行简单的state base test(见idior的Enterprise Test Driven Develop),可别对这个断言类期望太高,在实际使用中,我们往往需要自己编写一些高级断言。
常用的NUnit断言有:

method

usage

example

Assert.AreEqual(object expected, object actual[, string message])

验证两个对象是否相等

Assert.AreEqual(2, 1+1)

Assert.AreSame(object expected, object actual[, string message])

验证两个引用是否指向同意对象

object expected = new object();

object actual = expected;

Assert.AreSame(expected, actual)

Assert.IsFalse(bool)

验证bool值是否为false

Assert.IsFalse(false)

Assert.IsTrue(bool)

验证bool值是否为true

Assert.IsTrue(true)

Assert.IsNotNull(object)

验证对象是否不为null

Assert.IsNotNull(new object())

Assert.IsNull(object)

验证对象是否为null

Assert.IsNull(null);


这里要特殊指出的Assert.AreEqual只能处理基本数据类型和实现了Object.Equals接口的对象的比较,对于我们自定义对象的比较,通常需要自己编写高级断言,这个问题郁闷了我好一会,下面给出一个用于level=1的情况下的对象比较的高级断言的实现:

 1  public   class  AdvanceAssert
 2  {
 3       ///   <summary>
 4       ///  验证两个对象的属性值是否相等
 5       ///   </summary>
 6       ///   <remarks>
 7       ///  目前只支持的属性深度为1层
 8       ///   </remarks>
 9       public   static   void  AreObjectsEqual( object  expected,  object  actual)
10      {
11           //  若为相同引用,则通过验证
12           if  (expected  ==  actual)
13          {
14               return ;
15          }
16 
17           //  判断类型是否相同
18          Assert.AreEqual(expected.GetType(), actual.GetType());
19 
20           //  测试属性是否相等
21          Type t  =  expected.GetType();
22          PropertyInfo[] properties  =  t.GetProperties(BindingFlags.Instance  |  BindingFlags.Public);
23 
24           foreach  (PropertyInfo property  in  properties)
25          {
26               object  obj1  =  t.InvokeMember(property.Name, 
27                  BindingFlags.Instance  |  BindingFlags.Public  |  BindingFlags.GetProperty, 
28                   null , expected,  null );
29               object  obj2  =  t.InvokeMember(property.Name, 
30                  BindingFlags.Instance  |  BindingFlags.Public  |  BindingFlags.GetProperty, 
31                   null , actual,  null );
32 
33               //  判断属性是否相等
34              AdvanceAssert.AreEqual(obj1, obj2,  " assertion failed on  "   +  property.Name);
35          }
36      }
37 
38       ///   <summary>
39       ///  验证对象是否相等
40       ///   </summary>
41       private   static   void  AreEqual( object  expected,  object  actual,  string  message)
42      {
43          Type t  =  expected.GetType();
44 
45           if  (t.Equals( typeof (System.DateTime)))
46          {
47              Assert.AreEqual(expected.ToString(), actual.ToString(), message);
48          }
49           else
50          {
51               //  默认使用NUnit的断言
52              Assert.AreEqual(expected, actual, message);
53          }
54      }
55  }
56 


四、常用单元测试工具介绍:
1、NUnit:目前最高版本为2.2.7(也是本文所使用的NUnit的版本)
下载地址:http://www.nunit.org

2、TestDriven.Net:一款把NUnit和VS IDE集成的插件
下载地址:http://www.testdriven.net/

3、NUnit2Report:和nant结合生成单元测试报告
下载地址:http://nunit2report.sourceforge.net

4、Rhino Mocks 2:个人认为时.net框架下最好的mocks库,而且支持.net 2.0, rocks~!
下载地址:http://www.ayende.com/projects/rhino-mocks.aspx


想不到一口气写了这么多,前段时间在公司的项目中进行了一次单元测试的尝试,感触很深,看了idior的文章后更加觉得单元测试日后会成为项目的必需部分。在后续的文章中,我将讨论mocks,自定义测试框架和自动化测试工具,希望能和园子里的uter多多讨论。

你可能感兴趣的:(.net,object,测试,单元测试,attributes,单元测试工具)