1、简介
编写单元测试是一种验证行为,更是一种设计行为。同样,它更是一种编写文档的行为。编写单元测试避免了相当数量的反馈循环,尤其是功能验证方面的反馈循环。
什么是Unit Tests(单元测试)?
在程序设计过程中会有许多种测试,单元单元测试只是其中的一种,并不能保证程序是完美无缺的,但是在所有的测试中,单元测试是第一个环节,也是最重要的一个环节。单元测试是一种由程序员自行测试的工作。测试代码撰写者依据其所设想的方式执行是否产生了预期的结果。
NUnit是个.Net平台上的自动化单元测试框架,用来帮助代码撰写者方便的完成单元测试工作,它的下载地址是:http://www.nunit.org。
2、NUnit Framework(NUnit 单元测试框架)简介
NUnit是.net平台上使用得最为广泛的测试框架之一,下文将通过示例来描述NUnit的使用方法,并提供若干编写单元测试的建议和技巧,供单元测试者参考。
在NUnit 2.1里面,有一个Test Runner Application(负责执行Unit Tests的程序),这个Test Runner会扫描你已经compile(编译)好的程序代码,并且从Attribute里面知道哪些classes是test classes,哪些methods是需要执行的test methods. 然后,Test Runner使用.NET的Reflection技术来执行这些test methods。因为这个原因,你就不再需要让你的test classes继承自所谓的common base class。你唯一需要作的事,就是使用正确的Attribute来描述你的test classes及test methods。
NUnit提供了许多不同的attributes,让你可以自由的写你想要的unit tests。这些attributes可以用来定义test fixtures、test methods,以及setup及teardown的methods(预备及善后工作的methods)。除此之外,还有其它的attributes可以来设定预期发生的exceptions,或者要求Test Runner跳过某些test method不执行。
3、NUnit的基本用法
NUnit框架使用Attribute来描述测试用例的。只要掌握了 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标记的用法。下图是一个存储在数据库中的数字类:
下面,开始尝试为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()的测试函数
1 ///
2 /// regular test of DigitDataProvider.GetAllDigits()
3 ///
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要求每一个测试函数都可以独立运行,这就要求我们在调用目标函数之前先要初始化目标函数执行所需要的环境,如打开数据库连接、添加测试数据等。为了不影响其他的测试函数,在调用完目标函数后,该测试函数还要负责还原初始环境,如删除测试数据和关闭数据库连接等。对于同一测试类里的测试函数来说,这些操作往往是相同的,对上面的代码进行一次Refactoring, Extract Method:
1 ///
2 /// connection to database
3 ///
4 private static IDbConnection _conn;
5
6 ///
7 /// 初始化测试类所需资源
8 ///
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 ///
19 /// 释放测试类所占用资源
20 ///
21 [TestFixtureTearDown]
22 public void ClassCleanUp()
23 {
24 DigitProviderTest._conn.Close();
25 }
26
27 ///
28 /// 初始化测试函数所需资源
29 ///
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 ///
46 /// 释放测试函数所需资源
47 ///
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 ///
59 /// regular test of DigitDataProvider.GetAllDigits()
60 ///
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 ///
2 /// regular test of DigitDataProvider.RemovePrimeDigits
3 ///
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 ///
2 /// Exception test of DigitDataProvider.GetDigit()
3 ///
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里执行,欣赏一下黄绿灯吧。
4、测试函数的组织
现有一个性能测试的Testcase,执行一次要花上一个小时,我们并不需要(也无法忍受)每次自动化测试时都去执行这样的Testcase,使用NUnit的Explicit标记可以让这个TestCase只有在显示调用下才会执行:
[Test, Explicit]
public void OneHourTest()
{
//
}
这样耗时的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才会执行
5、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接口的对象的比较,对于自定义对象的比较,通常需要自己编写高级断言。
6、如何进行单元测试
下面通过简单的例子来演示一下在应用开发的过程中如何编写单元测试。在演示之前,已经安装了Nunit或者VSNunit等单元测试工具。
程序中有一个Users类,它对应的是数据库中的一个Users表。使用另外一个类EntityControl来通过ORM的方法把这个类中的数据通过增删改的方法与数据库中的数据进行同步,这里我们只需要知道它有这个功能就足够。这两个类提供的方法,参见下面的类图:
下面来看如何对这两个遍写单元测试代码。
先建立一个类---UnitTest.cs,在这个类中,加入下面的名字空间:
using NUnit.Framework;
在类名前面加上一个Attribute—TestFixture,Nunit在看到这个后,就会把这个类当作单元测试的类来处理。
[TestFixture]
public class UnitTest
下面增加下面的代码:
private EntityControl control;
[SetUp]
public void SetUp()
{
control = EntityControl.CreateControl();
}
[Setup]的作用就是在单元测试时,提供一些初始化的数据,在后面的测试方法中,就可以使用这个数据,类似于类中的Constructor.
下面来看如何测试增加用户的方法:
[Test]
public void AddTest()
{
users user = new users();
user.LogonID = "1216";
user.Name = "xian city1";
user.EmailAddress = "[email protected]";
control.AddEntity(user);
users u2 =(users) control.GetEntity(typeof(users),user.LogonID);
Assert.IsTrue(
u2.Name.Equals("xian city1") &&
u2.EmailAddress.Equals("[email protected]")
);
Assert.IsFalse(
u2.Name.Equals("xian city") &&
u2.EmailAddress.Equals("[email protected]")
);
}
上面的测试方法中,首先定义一个新用户,然后用AddEntity把它增加到数据中,为了验证增加到数据库中的数据是否正确,我们通过GetEntity方法根据主键再把它取出来。通过与我们刚才输入的数据进行比较来判断是否正确,在这里测试的时候,我们进行了两次测试,一个用正确的数据,一个用错误的数据,其目的是保证这个测试真正起到作用。
测试修改的方法和这个很类似:
[Test]
[Ignore("Finished Test")]
public void UpdateTest()
{
users u1 =(users) control.GetEntity(typeof(users),"112");
Assert.IsTrue(
u1.Password == "123" &&
u1.EmailAddress == "234"
);
u1.Password = "aaa";
u1.EmailAddress = "tim";
control.UpdateEntity(u1,"112");
Assert.IsFalse(
u1.Password == "123" ||
u1.EmailAddress == "234"
);
Assert.IsTrue(
u1.Password == "aaa" &&
u1.EmailAddress == "tim"
);
}
测试修改时,修改前和修改后都写了测试代码,这样就可以保证修改有效了。
从上面的方法可以看出,单元测试代码非常好写,而通过测试代码,要求开发人员对自己程序的测试结果有明确的认识。而测试方法设计的好坏与否直接影响到你的测试是否能够找出真正的错误。一般编写测试代码时,至少要把正常的情况和边界情况都测试一遍。
7、外代单元测试写法
1数据准备:
测试要先对所要测试的流程数据进行准备,测试仅对流程进行,对界面上的新增,修改及删除不进行测试;
主要是通过XML方式来准备,先在数据库中录入相应的数据,再用工具直接生成XML文件;
例:船务系统
先运行程序,新增船舶规范数据和预报数据,这样会在数据表:CDFLIB.CDVSLPF和SDFLIB.SDMASTPF两个表中存在相应的数据;
打开外代提供的工具(Penavicoxm 框架工具)à查询 查出所需数据,如 SELECT * FROM CDFLIB.CDVSLPF WHERE VSCODE=’所需代码’,在列表可以看到相应的数据,按”转化XML”保存文件; t;
2 命名规范:
测试项目命名: SBS_ + 子系统名称 + _TEST
类名命名: 对应类 + Test
XML文件: 一般命名为:SBS_ + 子系统名称 + 表名 + _Tes
3 项目创建:
先创建一个C# 测试项目;
创建类类:右键项目à ADDàNewTest
创建后默认Class上面有一个属性[TestClass],而方法有[TestMethod],用于标记类是测试类及测试方法;
例:创建一个确报测试方法:
///
/// 确报
///
[TestMethod]
public void SetActualArriveTest()
{
}
4 代码编写:
(1)从XML中加载准备数据,建议调用通用方法: Operate.GetXmlDS(“”);
(2)调用对应测试类的方法删除数据库中已经存在的测试数据;
(3)按照相应的逻辑调用外观层所要测试的方法
注意:所有流程性代码必须放在外观层才能进行测试
(4)逻辑运行成功后,取得结果进行判断,校验是否达到预期的目的;
5 示例代码:
///
/// 确报
///
[TestMethod]
public void SetActualArriveTest()
{
//插入船舶规范表(先删除)
DataSet dsCDVSLPF = Operate.GetXmlDS(@"SD_SBS_CDVSLPF.XML");
if(dsCDVSLPF != null && dsCDVSLPF.Tables.Count > 0)
{
CDVSLPFDeleteObject del = new CDVSLPFDeleteObject();
del.Where.SetAdditional(CDVSLPFDeleteObject.VSCODE_Field);
del.Where[CDVSLPFDeleteObject.VSCODE_Field].FieldValue = dsCDVSLPF.Tables[0].Rows[0]["VSCODE"].ToString().Trim();
CDVSLPFDeleteObject del2 = new CDVSLPFDeleteObject();
del2.Where.SetAdditional(CDVSLPFDeleteObject.VSCODE_Field);
del2.Where[CDVSLPFDeleteObject.VSCODE_Field].FieldValue = dsCDVSLPF.Tables[0].Rows[1]["VSCODE"].ToString().Trim();
shippingSystem.DeleteTwoOBJ(del, del2);
}
CDVSLPFDataSet insertOBJ = new CDVSLPFDataSet();
Assert.IsTrue(Operate.Load(insertOBJ, dsCDVSLPF, "CDFLIB.CDVSLPF"), "加载船舶规范表出错!");
Assert.IsTrue(shippingSystem.InsertData(insertOBJ), "船舶规范表插入失败!");
//插入船务中心主库(先删除)
DataSet dsSDMASTPF = Operate.GetXmlDS(@"SD_SBS_SDMASTPF.XML");
if (dsSDMASTPF != null && dsSDMASTPF.Tables.Count > 0)
{
SDMASTPFDeleteObject del = new SDMASTPFDeleteObject();
del.Where.SetAdditional(SDMASTPFDeleteObject.SDMID_Field);
del.Where[SDMASTPFDeleteObject.SDMID_Field].FieldValue = dsSDMASTPF.Tables[0].Rows[0]["SDMID"].ToString().Trim();
shippingSystem.DeleteData(del);
}
SDMASTPFDataSet dsInsert = new SDMASTPFDataSet();
Assert.IsTrue(Operate.Load(dsInsert, dsSDMASTPF, "SDFLIB.SDMASTPF"), "加载船务中心主库出错!");
Assert.IsTrue(shippingSystem.InsertData(dsInsert), "船务中心主库插入失败!");
//测试确报
Assert.IsTrue(shippingSystem.SetActualArrive("201006080125", dsSDMASTPF.Tables[0].Rows[0]["SDMID"].ToString().Trim(),
dsSDMASTPF.Tables[0].Rows[0]["SDMUSR"].ToString().Trim()), "确报失败!");
//判断流程参数
//主库校验动态是否正确
DataSet dsResult = shippingSystem.GetSDMASTPF( dsSDMASTPF.Tables[0].Rows[0]["SDMID"].ToString().Trim());
Assert.IsNull(dsResult, "确报数据未插入!");
Assert.Equals(dsSDMASTPF.Tables[0].Rows[0]["SDLAST"].ToString().Trim(), dsResult.Tables[0].Rows[0]["SDLAST"].ToString().Trim());
//动态校验动态是否正确
DataSet dsResult1 = shippingSystem.GetSDLASTPF(dsSDMASTPF.Tables[0].Rows[0]["SDMID"].ToString().Trim());
Assert.IsNull(dsResult, "动态未插入对应数据!");
Assert.Equals(dsResult1.Tables[0].Rows[0]["SDLAST"].ToString().Trim(), "EA");
}