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标记的用法。来看看一个存储在数据库中的数字类:
这是我们常见的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引用:
并在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
}
不幸的是,这样耗时的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才会执行
三、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多多讨论。