实践单元测试(3)-Using NUnit
继续下文之前,先来看看一个非常简单的测试用例(TestCase):
2 public void AdditionTest()
3 {
4 int expectedResult = 2;
5
6 Assert.AreEqual(exptectedResult, 1 + 1);
7 }
主要内容:
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标记的用法。来看看一个存储在数据库中的数字类:
这是我们常见的DAL+Entity的设计,DigitDataProvider和Digit类的实现代码如下:
1)Digit.cs类:
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类:
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)新建测试数据库:
[DigitID] [uniqueidentifier] NOT NULL ,
[Value] [int] NOT NULL
) ON [PRIMARY]
GO
下面,我们开始尝试为DigitDataProvider类编写UT,新建DigitDataProviderTest.cs类。
1、添加nunit.framework引用:
并在DigitDataProviderTest.cs中添加:
2、编写测试用例
1)标识测试类:NUnit要求每个测试类都必须添加TestFixture的Attribute,并且携带一个public无参构造函数。
2 public class DigitProviderTest
3 {
4 public DigitProviderTest()
5 {
6 }
7 }
2)编写DigitDataProvider.GetAllDigits()的测试函数
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 }
什么?很丑?很麻烦?这个问题稍后再讨论,先来看看一个完整的测试用例该如何定义:
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 }
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 }
TestFixtureSetup:在当前测试类中的所有测试函数运行前调用;
TestFixtureTearDown:在当前测试类的所有测试函数运行完毕后调用;
Setup:在当前测试类的每一个测试函数运行前调用;
TearDown:在当前测试类的每一个测试函数运行后调用。
3)编写DigitDataProvider.RemovePrimeDigits()的测试函数
唉,又忘了质数判断的算法,这个函数先不实现(throw new NotImplementedException()),对应的测试函数先忽略。
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 }
4)编写DigitDataProvider.GetDigit()的测试函数
当查找一个不存在的Digit实体时,GetDigit()会不会像我们预期一样抛出NullReferenceExceptioin呢?
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(Type t, string expectedMessage)
在NUnitConsoler里执行一把,欣赏一下黄绿灯吧。本文相关代码可从UTDemo_Product.rar下载。
二、测试函数的组织
现在有一个性能测试的Testcase,执行一次要花上一个小时,我们并不需要(也无法忍受)每次自动化测试时都去执行这样的Testcase,使用NUnit的Explicit标记可以让这个TestCase只有在显示调用下才会执行:
2 public void OneHourTest()
3 {
4 //
5 }
不幸的是,这样耗时的TestCase在整个测试工程中可能有数十个,或许更多,我们能不能把这些TestCase都组织起来,要么一起运行,要么不运行呢?NUnit提供的Category标记可实现此功能:
2 public void OneHourTest()
3 {
4 ...
5 }
6
7 [Test, Explicit, Category("LongTest")]
8 public void TwoHoursTest()
9 {
10 ...
11 }
三、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的情况下的对象比较的高级断言的实现:
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~~