一、单元测试是什么
单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证。对于单元测试中单元的含义,一般来说,要根据实际情况去判定其具体含义,如C语言中单元指一个函数,C#里单元指一个类,图形化的软件中可以指一个窗口或一个菜单等。总的来说,单元就是人为规定的最小的被测功能模块。单元测试是在软件开发过程中要进行的最低级别的测试活动,软件的独立单元将在与程序的其他部分相隔离的情况下进行测试。
单元测试(模块测试)是开发者编写的一小段代码,用于检验被测代码的一个很小的、很明确的功能是否正确。通常而言,一个单元测试是用于判断某个特定条件(或者场景)下某个特定函数的行为。
二、为什么需要单元测试
在我们现在的编程思维中一直都是编码=>编译=>调试,一直循环,直到要处理的功能完成,每一个功能完成都是如此,且有的功能是严重依赖于上一个功能。在如此处理中存在几个问题。
-
编译通过后,运行程序出现的bug难以定位。
-
修改一个bug,容易引进其他bug。
-
Bug越到后期发现,越难以修改。
-
后期系统的复杂性,导致代码难以修改和重构,使得系统难以维护。
-
开发人员常认为编译功过,进行了几次手工测试就等于测试通过(认为详细的测试是测试人员的工作,非开发人员的工作)。
-
在完全依赖外部系统的情况下,难以进行有效的测试。
-
手工测试效率低下,针对性不强,测试不能重用。
有了单元测试在开发过程中起到的作用。
-
大大节约了测试和修改的时间,有效且便于测试各种情况。
-
能快速定位bug(每一个测试用例都是具有针对性)。
-
能使开发人员重新审视需求和功能的设计(难以单元测试的代码,就需要重新设计)。
-
强迫开发者以调用者而不是实现者的角度来设计代码,利于代码之间的解耦。
-
自动化的单元测试能保证回归测试的有效执行。
-
使代码可以放心修改和重构。
-
测试用例,可作为开发文档使用(测试即文档)。
-
测试用例永久保存,支持随时测试。
既然单元测试有这些好处,为什么我们不去用呢。可以归纳为以下几个理由。
-
对单元测试存在的误解,如:单元测试属于测试工作,应该由测试人员来完成,所以单元测试不属于开发人员的职责范围。答:虽然单元测试虽然叫做"测试",但实际属于开发范畴,应该由开发人员来做,而开发人员也能从中受益。
-
没有真正意识到单元测试的收益,认为写单元测试太费时,不值得。
答:在开发时越早发现bug,就能节省更多的时间,降低更多的风险。单元测试先期要编写测试用例,是需要多耗费些时间,但是后面的调试、自测,都可以通过单元测试处理,不用手工一遍又一遍处理。实际上总时间被减少了。
-
项目经理或技术主管没有要求写单元测试,所以不用写。
答:写单元测试应该成为开发人员的一种本能,开发本身就应该包含单元测试。
-
不知道有单元测试这回事,不知道如何用。经过这篇文档的说明,就基本知道如何处理单元测试。
结论:
只进行手工测试,只是临时性的单元测试,代码测试覆盖率要超过70%都很困难,未覆盖的代码可能遗留大量的细小的错误,这些错误还会互相影响,当bug暴露出来的时候难于调试,大幅度提高后期测试和维护成本。可以说,进行充分的单元测试,是提高软件质量,降低开发成本的必由之路。
要进行充分的单元测试,应专门编写测试代码,并与产品代码隔离。比较简单的办法是为产品工程建立对应的测试工程,为每个类建立对应的测试类,为每个函数(很简单的除外)建立测试函数。
单元测试是由程序员自己来完成,最终受益的也是程序员自己。可以这么说,程序员有责任编写功能代码,同时也就有责任为自己的代码编写单元测试。执行单元测试,就是为了证明这段代码的行为和我们期望的一致。
对于程序员来说,如果养成了对自己写的代码进行单元测试的习惯,不但可以写出高质量的代码,而且还能提高编程水平。
三、单元测试工具。
在.Net平台有三种单元测试工具,分别为MS Test、NUnit、Xunit.Net。
1.MS Test为微软产品,集成在Visual Studio 2008+工具中。
2.NUnit为.Net开源测试框架(采用C#开发),广泛用于.Net平台的单元测试和回归测试中,官方网址(www.nunit.org)。
3.XUnit.Net为NUnit的改进版。
(以下主要讲解NUnit的使用,会了NUnit其他2个测试工具也能快速熟悉)。
任何xUnit工具都使用断言进行条件的判断,NUnit自然也不例外,与其它的xUnit(如JUnit、phpUnit、pythonUnit)相比,由于大量使用了Generic、Attribute等语言特征,NUnit提供了更为方面、灵活的测试方法,下面先介绍一下断言。
NUnit一共有五个断言类,分别是Assert、StringAssert、FileAssert、DirectoryAssert、CollectionAssert,它们都在NUnit.Framework命名空间,其中Assert是常用的,而另外四个断言类,顾名思义,分别对应于字符串的断言、文件的断言、目录的断言、集合的断言。理论上,仅Assert类就可以完成所有条件的判断,然而,如果合理的运用后面的四个断言,将使代码更加简洁、美观,也更加便于理解和维护。
四、NUnit的使用。
本处演示所使用的NUnit版本为2.6.4,若要使用最新版可以去官网下载。
首先创建一个类库项目(也可以是其他项目),然后创建一个Test+类库名称的项目(也可以是项目名称+Test),用于代表是测试工程。如下图:
Demonstration项目中含有一个计算功能类,对应的测试项目含有一个测试计算类,一个计算功能类中方法可能需要多个测试用例来完成检测。如下展示出了2个类的代码:
/// <summary> /// 用于演示的一个简单计算功能 /// </summary> public class Calculate { /// <summary> /// 加法 /// </summary> public int Add(int a, int b) { return a + b; } /// <summary> /// 减法 /// </summary> public int Subtract(int a, int b) { return a - b; } /// <summary> /// 乘法 /// </summary> public int Multiply(short a, short b) { return a * b; } /// <summary> /// 除法 /// </summary> public int Quotient(int a, int b) { return a / b; } /// <summary> /// 开平方根 /// </summary> public double SquareRoot(int num) { return Math.Sqrt(num); } /// <summary> /// 四舍五入,取整 /// </summary> public int Round_Off(double num) { return (int)Math.Round(num); } /// <summary> /// 向上取整 /// </summary> public int UpwardTrunc(double num) { return (int)Math.Ceiling(num); } /// <summary> /// 平方 /// </summary> public int Square(short num) { throw new NotImplementedException(); } } [TestFixture(Description = "测试示例")] public class TestCalculate { private Calculate calculate; private StreamReader reader; private string[] sourceData = new string[] { @"......Resourcescore_1.csv" }; private short a, b; [TestFixtureSetUp] public void Initialize() { Console.WriteLine("初始化信息"); calculate = new Calculate(); } [TestFixtureTearDown] public void Dispose() { Console.WriteLine("释放资源"); if (reader != null) { reader.Close(); } } [SetUp] public void SetUp() { a = 3; b = 2; } [TearDown] public void TearDown() { Console.WriteLine("我是清理者"); } [Test(Description = "加法")] [Category("优先级 1")] public void TestAdd() { Assert.AreEqual(5, calculate.Add(a, b)); } [Category("优先级 1")] [TestCase(1, 2), TestCase(2, 3)] public void TestSubtract(int a, int b) { Assert.AreEqual(a - b, calculate.Subtract(a, b)); } [Category("优先级 2")] [TestCase(1, 2, Result = 2), TestCase(2, 3, Result = 6)] public int TestMultiply(short a, short b) { return calculate.Multiply(a, b); } [Test] [Category("优先级 2")] [ExpectedException(typeof(DivideByZeroException))] public void TestQuotient() { calculate.Quotient(a, 0); } [Test] [Category("优先级 3")] public void TestSquareRoot() { Assert.Less(1, calculate.SquareRoot(a)); } [Test] [Category("优先级 3")] [Sequential] public void TestRound_Off([Values(3.4, 4.5, 4.6, 5.5)] double num, [Values(3, 5, 5, 6)] int result) { Assert.AreEqual(result, calculate.Round_Off(num)); } [Test] [Category("优先级 3")] public void TestUpwardTrunc([ValueSource("sourceData")] object fileName) { reader = new StreamReader((string)fileName); string content; while ((content = reader.ReadLine()) != null) { var nums = content.Split(',').Select(c => double.Parse(c)).ToArray(); Array.ForEach(nums, (num) => { int result = calculate.UpwardTrunc(num); Console.Write(result + " "); }); } } [Test] public void TestSquare() { Assert.Throws<NotImplementedException>(() => calculate.Square(b)); } [Test, Explicit] [Ignore] public void TestFactorial() { Assert.Fail("未能实现阶乘功能"); } }
在粗略看了代码后,下面就详细说明相应的测试标记(属性)的用法。
- [TestFixture(arguments)]属性标记类为测试类,若没有填写参数,则测试类必须含有无参构造函数,否则需要相应的有参构造函数。也可以多个测试[TestFixture(1), TestFixture("a")]
-
[Test]属性标记方法为测试方法,中添加Description参数可以给我们测试的功能添加描述信息。
-
[TestCase(arguments)]属性标记有参数无返值方法为测试方法(泛型方法一样标记),想要多次测试可用逗号隔开([TestCase(1,2), TestCase(2,3)])。
-
[TestCase(arguments,Result = value)属性标记带参数与返回值的方法为测试方法,执行的时候把预期的返回值也告诉NUnit,如果返回值不对,测试同样无法通过。
-
[Suite](测试套件,仅对属性与索引器标记有效):可以将多个测试类组合到一起,同时执行多个测试。本版本的开发人员的一个信念就是减少这个的需要,可以使用[Category]来替代它。
-
[Explicit]属性标记测试方法需要在UI界面显式执行,如果不想对某个方法进行单元测试,只是在它被选中时才进行测试的话,可以调用该特性。
-
[Ignore]属性标记一个测试方法或一个测试类被忽略,如果测试类被忽略,其内中的测试方法也会被忽略。
-
[ExpectedException(Type)]属性标记测试方法在运行时抛出一个期望的异常,如果是则测试通过,否则不通过。
-
[Category("")]属性标记用于将测试分类(便于只测试需要的类别),可在方法与类上进行标记,在NUnit-GUI界面的Categories选项卡中对要测试种类进行添加,Run时仅测试该类别的测试。
-
[TestFixtureSetUp]属性标记方法为类级别设置(初始化)方法,在整个测试类中执行一次初始化,所有的测试方法共享初始化数据。
-
[TestFixtureTearDown]属性标记方法为类级别拆卸方法,在整个测试类中执行一次拆卸.当测试类中的所有测试方法执行完成,就会执行拆卸方法,用于清除数据、释放资源。
-
[TearDown]属性标记方法为函数级别的拆卸方法,在执行完每个测试方法后,执行该拆卸方法。一个测试类可以仅有一个TearDown/Setup/TestFixtureSetUp/TestFixtureTearDown方法。如果有多个定义,测试类也会编译成功,但是测试时不会运行这些标记过的方法。
-
[SetUp]属性标记方法为函数级别的设置方法,在执行每个测试方法前,执行该设置方法。
-
每执行一次Run,就是new一个新的实例在测试。
-
[Maxtime]/[Timeout]属性标记测试用例的最大执行时间,前者超时时不取消测试,而后者会强行中断,用法如:[Test, Maxtime(2000)],[Test, Timeout(2000)]。
-
[Repeat]属性标记测试方法重复执行多少次,如:[Test, Repeat(100)]。
-
[RequiresMTA]/[RequiresSTA]/[RequiresThread]属性标记测试用例必须的在多线程、单线程、独立的线程状态下运行。
-
[Values]属性标记测试用例的参数,以参数的形式传入一组值,NUnit会把这组值分解成相应数量的子测试。当测试用例的2个参数都使用[Values]进行标记,NUnit默认生成2组数量乘积的用例,需要使用[Sequential]标记测试用例才能按顺序生成一一对应的n(n=2组中最大数组长度)个子测试用例。
-
[ValueSource]属性标记测试用例的参数,指定参数的数据源来自哪里,在使用[ValueSource]指定数据源时,该数据源必须实现了IEnumerable接口,数据源可以是属性、无参方法、实例或静态成员。
更多属性标记与详细说明,可以查阅NUnit官网提供的说明文档。一个方法的测试可能要写很多个测试用例,这都是正常的,如果一个测试用例包含多个断言,那些紧跟失败断言的断言都不会执行,因为通常每个测试方法最好只有一个断言。
在运行单元测试时有3种方式分别为:
-
把测试工程的属性=>调试=>启动外部程序,设置为NUnit运行程序。在启用调试时会启动NUnit界面程序,但NUnit界面没有测试用例的信息,需要自己添加在File=>Open Project->文件资源管理器,找你的测试工程类库或程序添加即可。点击Run运行,根据选中的节点运行该节点下所有的子测试用例(该测试可进行调试)。如下图:
以上的图片展示了运行错误界面和运行输出界面。在测试用例的节点中绿色'√'代表通过,黄色'√'代表忽略,红色'×'代表失败。
-
直接启动NUnit界面程序,在File=>Open Project->文件资源管理器,添加测试工程类库或程序,点击相应的节点进行Run测试,NUnit会根据类库或程序生成更新,自动更新界面中测试用例节点,但运行的测试用例不能进行调试。效果图与①中的效果一样。
-
在Visual Studio 2010+的IDE中以插件的方式集成NUnit测试工具,直接在测试工程中点击鼠标右键,运行测试即可。或者在VS菜单栏的测试中运行NUnit测试。集成与运行效果图在"第五节"中展示。
五、Nunit常用类和方法
1、Assert(断言):如果断言失败,方法将没有返回,并且报告一个错误。
1)、测试二个参数是否相等
Assert.AreEqual;
Assert.AreEqual;
2)、测试二个参数是否引用同一个对象
Assert.AreSame;
Assert.AreNotSame;
3)、测试一个对象是否被一个数组或列表所包含
Assert.Contains;
4)、测试一个对象是否大于另一个对象
Assert.Greater;
5)、测试一个对象是否小于另一个对象
Assert.Less;
6)、类型断言:
Assert.IsInstanceOfType;
Assert.IsAssignableFrom;
7)、条件测试:
Assert.IsTrue;
Assert.IsFalse;
Assert.IsNull;
Assert.IsNotNull;
Assert.IsNaN;用来判断指定的值是否为数字。
Assert.IsEmpty;
Assert.IsNotEmpty;
Assert.IsEmpty;
Assert.IsNotEmpty;
8)、其他断言:
Assert.Fail;方法为你提供了创建一个失败测试的能力,这个失败是基于其他方法没有封装的测试。对于开发你自己的特定项目的断言,它也很有用。
Assert.Pass;强行让测试通过
2、字符串断言(StringAssert):提供了许多检验字符串值的有用的方法
StringAssert.Contains;
StringAssert.StartsWith;
StringAssert.EndsWith;
StringAssert.AreEqualIgnoringCase;
3、CollectionAssert类
CollectionAssert.AllItemsAreInstancesOfType;集合中的各项是否是某某类型的实例
CollectionAssert.AllItemsAreNotNull:集合中的各项均不为空
CollectionAssert.AllItemsAreUnique;集合中的各项唯一
CollectionAssert.AreEqual;两个集合相等
CollectionAssert.AreEquivalent;两个集合相当
CollectionAssert.AreNotEqual;两个集合不相等
CollectionAssert.AreNotEquivalent;两个集合不相当
CollectionAssert.Contains;
CollectionAssert.DoesNotContain;集合中不包含某对象
CollectionAssert.IsSubsetOf:一个集合是另外一个集合的子集
CollectionAssert.IsNotSubsetOf:一个集合不是另外一个集合的子集
CollectionAssert.IsEmpty;集合为空
CollectionAssert.IsNotEmpty;集合不为空
CollectionAssert.IsOrdered;集合的各项已经排序
4、FileAssert
FileAssert.AreEqual;
FileAssert.AreNotEqual;
5、DirectoryAssert
DirectoryAssert.AreEqual;
DirectoryAssert.AreNotEqual;
DirectoryAssert.IsEmpty;
DirectoryAssert.IsNotEmpty;
DirectoryAssert.IsWithin;
DirectoryAssert.IsNotWithin;
六、NUnit集成到VS中的使用。
在使用NUnit-GUI处理运行测试用例,是不是感觉比较麻烦,还要使用外部的NUnit应用程序,有没有简单点的最好能够跟VS开发工具紧密结合的方式来进行NUnit单元测试呢?答案是肯定的,有2种方式。
1.我们在VS中选择工具菜单栏下的扩展和更新,选择联机并在搜索框中输入NUnit。出现如下图的信息,有2个版本的Nunit适配器,分别为NUnit 3.x(最新版为3.4.1)和NUnit 2.x(最新版为2.6.4),都支持Visual Studio 2012+。若想在VS2010中集成,需要安装NUnit 2.6.4安装包(可在官网下载)与VS2010 NUnit整合插件(下载地址:
http://visualstudiogallery.msdn.microsoft.com/c8164c71-0836-4471-80ce-633383031099),下载安装完毕就能在 VS2010 的视图=>其他窗口中看到 Visual Nunit(或使用快捷键Ctrl + F7),打开该视图,将之拖到合适的位置。
下载安装NUnit Test Adapter后关闭VS,重启一下就好了,我们打开类库项目中的TestCalculate类,在右键弹出的菜单中点击运行测试。运行结束后,会在左侧的测试资源管理器当中显示本次操作的结果。
2.通过ReSharper工具处理NUnit的单元测试,在VS2010+中安装了ReSharper开发插件,ReSharper内中自带支持NUnit与MS Test这2个单元测试工具,只要你的测试工程中引用了相应的单元测试类库(如nunit.Framework.dll)、以及含有测试用例。通过鼠标右键或快捷键(Ctrl + T,R),就可以运行单元测试,也可以进行单元测试调试,ReSharper选项图与运行效果如下图。
七、后续
上面列出只能单元测试的基本使用,未能说明对Mock等其他功能的使用,也没有解释对难以单元测试的代码进行重新设计的说明,需要后期深入了解才能列出相应的文档说明。能够更好的使用单元测试才能更好的使用TDD(测试驱动开发)来开展项目,TDD测试驱动开发是测试先行(此测试是单元测试)、是极限编程的一个重要特点,它以不断的测试推动代码的开发,既简化了代码,同时也保证了软件指令,另一方面说编写的测试用例将成为重要文档(可以作为SDK提供给开发者,测试即文档)。
-----------------以上内容是根据博客园其他博客的说明与Nunit官方文档,以及自己测试使用,进行了整理说明。----------------------------