实践单元测试(3)-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标记的用法。来看看一个存储在数据库中的数字类:
实践单元测试(3)-Using NUnit
这是我们常见的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引用
实践单元测试(3)-Using NUnit
并在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      实践单元测试(3)-Using NUnit
 6 
 7       //  2) prepare test data, if neccessary
 8      实践单元测试(3)-Using NUnit
 9 
10       //  3) test the production code by using assertion or Mocks.
11      实践单元测试(3)-Using NUnit
12 
13       //  4) clear test data
14      实践单元测试(3)-Using NUnit
15 
16       //  5) reset the environment
17      实践单元测试(3)-Using NUnit
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  }

实践单元测试(3)-Using NUnitIgnore的用法:

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  }

实践单元测试(3)-Using NUnitExpectedException的用法

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



在NUnitConsoler里执行一把,欣赏一下黄绿灯吧实践单元测试(3)-Using NUnit。本文相关代码可从UTDemo_Product.rar下载。

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

1  [Test, Explicit]
2  public   void  OneHourTest()
3  {
4      //实践单元测试(3)-Using NUnit
5  }


实践单元测试(3)-Using NUnit

不幸的是,这样耗时的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才会执行
实践单元测试(3)-Using NUnit

三、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);


实践单元测试(3)-Using NUnit这 里要特殊指出的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多多讨论。

好向往TDD~~实践单元测试(3)-Using NUnit

你可能感兴趣的:(单元测试)