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多多讨论。
好向往TDD~~