Nunit单元测试
NUnit是.net平台上使用得最为广泛的测试框架之一,本文将通过示例来描述NUnit的使用方法,并提供若干编写单元测试的建议和技巧,供单元测试的初学者参考。
继续下文之前,先来看看一个非常简单的测试用例(TestCase):
[Test] public void AdditionTest() { int expectedResult = 2; Assert.AreEqual(exptectedResult, 1 + 1); }
你肯定会说这个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类:
using System; using System.Data; namespace Product { /// <summary> /// Digit 的摘要说明 /// </summary> /// 创 建 人: 罗旭成 /// 创建日期: 2013-10-22 /// 修 改 人: /// 修改日期: /// 修改内容: /// 版 本: public class Digit { private Guid _digitId; public Guid DigitID { get { return this._digitId; } set { this._digitId = value; } } private int _value = 0; public int Value { get { return this._value; } set { this._value = value; } } #region 构造函数 /// <summary> /// 默认无参构造函数 /// </summary> /// 创 建 人: 罗旭成 /// 创建日期: 2013-10-22 /// 修 改 人: /// 修改日期: /// 修改内容: public Digit() { // // TODO: 在此处添加构造函数逻辑 // } /// <summary> /// construct the digit object from a datarow /// </summary> /// <param name="row"></param> public Digit(DataRow row) { if (row == null) { throw new ArgumentNullException(); } if (row["DigitID"] != DBNull.Value) { this._digitId = new Guid(row["DigitID"].ToString()); } if (row["Value"] != DBNull.Value) { this._value = Convert.ToInt32(row["Value"]); } } #endregion } }
2)DigitDataProvider类:
using System; using System.Data; using System.Data.SqlClient; using System.Collections; namespace Product { /// <summary> /// DigitDataProvider 的摘要说明 /// </summary> /// 创 建 人: 罗旭成 /// 创建日期: 2013-10-22 /// 修 改 人: /// 修改日期: /// 修改内容: /// 版 本: public class DigitDataProvider { /// <summary> /// 定义数据库连接 /// </summary> private SqlConnection _dbConn; public SqlConnection Connection { get { return this._dbConn; } set { this._dbConn = value; } } #region 构造函数 /// <summary> /// 默认无参构造函数 /// </summary> /// 创 建 人: 罗旭成 /// 创建日期: 2013-10-22 /// 修 改 人: /// 修改日期: /// 修改内容: public DigitDataProvider() { // // TODO: 在此处添加构造函数逻辑 // } public DigitDataProvider(SqlConnection conn) { this._dbConn = conn; } #endregion #region 成员函数定义 /// <summary> /// retrieve all Digits in the database /// </summary> /// <returns></returns> public ArrayList GetAllDigits() { // retrieve all digit record in database SqlCommand command = this._dbConn.CreateCommand(); command.CommandText = "SELECT * FROM digits"; SqlDataAdapter adapter = new SqlDataAdapter(command); DataSet results = new DataSet(); adapter.Fill(results); // convert rows to digits collection ArrayList digits = null; if (results != null && results.Tables.Count > 0) { DataTable table = results.Tables[0]; digits = new ArrayList(table.Rows.Count); foreach (DataRow row in table.Rows) { digits.Add(new Digit(row)); } } return digits; } /// <summary> /// remove all digits from the database /// </summary> /// <returns></returns> public int RemoveAllDigits() { // retrieve all digit record in database SqlCommand command = this._dbConn.CreateCommand(); command.CommandText = "DELETE FROM digits"; return command.ExecuteNonQuery(); } /// <summary> /// retrieve and return the entity of given value /// </summary> /// <exception cref="System.NullReferenceException">entity not exist in the database</exception> /// <param name="value"></param> /// <returns></returns> public Digit GetDigit(int value) { // retrieve entity of given value SqlCommand command = this._dbConn.CreateCommand(); command.CommandText = "SELECT * FROM digits WHERE Value='" + value + "'"; SqlDataAdapter adapter = new SqlDataAdapter(command); DataSet results = new DataSet(); adapter.Fill(results); // convert rows to digits collection Digit digit = null; if (results != null && results.Tables.Count > 0 && results.Tables[0].Rows.Count > 0) { digit = new Digit(results.Tables[0].Rows[0]); } else { throw new NullReferenceException("not exists entity of given value"); } return digit; } /// <summary> /// remove prime digits from database /// </summary> /// <returns></returns> public int RemovePrimeDigits() { throw new NotImplementedException(); } #endregion } }
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中添加:
using NUnit.Framework;
2、编写测试用例
1)标识测试类:NUnit要求每个测试类都必须添加TestFixture的Attribute,并且携带一个public无参构造函数。
[TestFixture] public class DigitProviderTest { public DigitProviderTest() { } }
2)编写DigitDataProvider.GetAllDigits()的测试函数
/// <summary> /// regular test of DigitDataProvider.GetAllDigits() /// </summary> [Test] public void TestGetAllDigits() { // initialize connection to the database // note: change connection string to ur env IDbConnection conn = new SqlConnection( "Data source=localhost;user id=sa;password=sa;database=utdemo"); conn.Open(); // preparing test data IDbCommand command = conn.CreateCommand(); string commadTextFormat = "INSERT INTO digits(DigitID, Value) VALUES('{0}', '{1}')"; for (int i = 1; i <= 100; i++) { command.CommandText = string.Format( commadTextFormat, Guid.NewGuid().ToString(), i.ToString()); command.ExecuteNonQuery(); } // test DigitDataProvider.GetAllDigits() int expectedCount = 100; DigitDataProvider provider = new DigitDataProvider(conn as SqlConnection); IList results = provider.GetAllDigits(); // that works? Assert.IsNotNull(results); Assert.AreEqual(expectedCount, results.Count); // delete test data command = conn.CreateCommand(); command.CommandText = "DELETE FROM digits"; command.ExecuteNonQuery(); // close connection to the database conn.Close(); }
什么?很丑?很麻烦?这个问题稍后再讨论,先来看看一个完整的测试用例该如何定义:
[Test] public void TestCase() { // 1) initialize test environement, like database connection // 2) prepare test data, if neccessary // 3) test the production code by using assertion or Mocks. // 4) clear test data // 5) reset the environment }
NUnit要求每一个测试函数都可以独立运行(往往有人会误解NUnit并按照Consoler中的排序来执行),这就要求我们在调用目标函数之前先要初 始化目标函数执行所需要的环境,如打开数据库连接、添加测试数据等。为了不影响其他的测试函数,在调用完目标函数后,该测试函数还要负责还原初始环境,如 删除测试数据和关闭数据库连接等。对于同一测试类里的测试函数来说,这些操作往往是相同的,让我们对上面的代码进行一次Refactoring,Extract Method:
/// <summary> /// connection to database /// </summary> private static IDbConnection _conn; /// <summary> /// 初始化测试类所需资源 /// </summary> [TestFixtureSetUp] public void ClassInitialize() { // note: change connection string to ur env DigitProviderTest._conn = new SqlConnection( "Data source=localhost;user id=sa;password=sa;database=utdemo"); DigitProviderTest._conn.Open(); } /// <summary> /// 释放测试类所占用资源 /// </summary> [TestFixtureTearDown] public void ClassCleanUp() { DigitProviderTest._conn.Close(); } /// <summary> /// 初始化测试函数所需资源 /// </summary> [SetUp] public void TestInitialize() { // add some test data IDbCommand command = DigitProviderTest._conn.CreateCommand(); string commadTextFormat = "INSERT INTO digits(DigitID, Value) VALUES('{0}', '{1}')"; for (int i = 1; i <= 100; i++) { command.CommandText = string.Format( commadTextFormat, Guid.NewGuid().ToString(), i.ToString()); command.ExecuteNonQuery(); } } /// <summary> /// 释放测试函数所需资源 /// </summary> [TearDown] public void TestCleanUp() { // delete all test data IDbCommand command = DigitProviderTest._conn.CreateCommand(); command.CommandText = "DELETE FROM digits"; command.ExecuteNonQuery(); } /// <summary> /// regular test of DigitDataProvider.GetAllDigits() /// </summary> [Test] public void TestGetAllDigits() { int expectedCount = 100; DigitDataProvider provider = new DigitDataProvider(DigitProviderTest._conn as SqlConnection); IList results = provider.GetAllDigits(); // that works? Assert.IsNotNull(results); Assert.AreEqual(expectedCount, results.Count); }
NUnit提供了以下Attribute来支持测试函数的初始化:
TestFixtureSetup:在当前测试类中的所有测试函数运行前调用;
TestFixtureTearDown:在当前测试类的所有测试函数运行完毕后调用;
Setup:在当前测试类的每一个测试函数运行前调用;
TearDown:在当前测试类的每一个测试函数运行后调用。
3)编写DigitDataProvider.RemovePrimeDigits()的测试函数
唉,又忘了质数判断的算法,这个函数先不实现(throw new NotImplementedException()),对应的测试函数先忽略。
/// <summary> /// regular test of DigitDataProvider.RemovePrimeDigits /// </summary> [Test, Ignore("Not Implemented")] public void TestRemovePrimeDigits() { DigitDataProvider provider = new DigitDataProvider(DigitProviderTest._conn as SqlConnection); provider.RemovePrimeDigits(); }
Ignore的用法:
Ignore(string reason)
4)编写DigitDataProvider.GetDigit()的测试函数
当查找一个不存在的Digit实体时,GetDigit()会不会像我们预期一样抛出NullReferenceExceptioin呢?
/// <summary> /// Exception test of DigitDataProvider.GetDigit() /// </summary> [Test, ExpectedException(typeof(NullReferenceException))] public void TestGetDigit() { int expectedValue = 999; DigitDataProvider provider = new DigitDataProvider(DigitProviderTest._conn as SqlConnection); Digit digit = provider.GetDigit(expectedValue); }
ExpectedException的用法
ExpectedException(Type t) ExpectedException(Type t, string expectedMessage)
在NUnitConsoler里执行一把,欣赏一下黄绿灯吧。本文相关代码可从UTDemo_Product.rar下载。
二、测试函数的组织
现在有一个性能测试的Testcase,执行一次要花上一个小时,我们并不需要(也无法忍受)每次自动化测试时都去执行这样的Testcase,使用NUnit的Explicit标记可以让这个TestCase只有在显示调用下才会执行:
[Test, Explicit] public void OneHourTest() { // }
不幸的是,这样耗时的TestCase在整个测试工程中可能有数十个,或许更多,我们能不能把这些TestCase都组织起来,要么一起运行,要么不运行呢?NUnit提供的Category标记可实现此功能:
[Test, Explicit, Category("LongTest")] public void OneHourTest() { ... } [Test, Explicit, Category("LongTest")] public void TwoHoursTest() { ... }
这样,只有当显示选中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的情况下的对象比较的高级断言的实现:
public class AdvanceAssert { /// <summary> /// 验证两个对象的属性值是否相等 /// </summary> /// <remarks> /// 目前只支持的属性深度为1层 /// </remarks> public static void AreObjectsEqual(object expected, object actual) { // 若为相同引用,则通过验证 if (expected == actual) { return; } // 判断类型是否相同 Assert.AreEqual(expected.GetType(), actual.GetType()); // 测试属性是否相等 Type t = expected.GetType(); PropertyInfo[] properties = t.GetProperties(BindingFlags.Instance | BindingFlags.Public); foreach (PropertyInfo property in properties) { object obj1 = t.InvokeMember(property.Name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.GetProperty, null, expected, null); object obj2 = t.InvokeMember(property.Name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.GetProperty, null, actual, null); // 判断属性是否相等 AdvanceAssert.AreEqual(obj1, obj2, "assertion failed on " + property.Name); } } /// <summary> /// 验证对象是否相等 /// </summary> private static void AreEqual(object expected, object actual, string message) { Type t = expected.GetType(); if (t.Equals(typeof(System.DateTime))) { Assert.AreEqual(expected.ToString(), actual.ToString(), message); } else { // 默认使用NUnit的断言 Assert.AreEqual(expected, actual, message); } } }
四、常用单元测试工具介绍:
1、NUnit:目前最高版本为2.6.2(也是本文所使用的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~~
个人主要研究:金融系统、MIS系统、人力资源管理系统、数据采集系统、权限管理系统等等系统。主攻C#开发语言,Oracle、Sql Server,WCF和Remoting通信。
如需联系可加QQ:442389681 Email:lxc880615@163.com 手机:18922735098
QQ群交流:186841119 (请注明来自博客园)
博客园地址:http://www.cnblogs.com/jara/ http://www.cnblogs.com/luoyuhao/
提示:本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
如果觉得还有帮助的话,可以点一下右下角的【推荐】,希望能够持续的为大家带来好的技术文章!想跟我一起进步么?那就【关注】我吧。
如果对文章有任何问题,都可以在评论中留言,我会尽可能的答复您,谢谢您的阅读