什么是单元测试?
单元测试是开发人员对其所实现功能的代码进行的另外编写的测试,用于检测其代码功能的完整性、正确性和其运行效率,从而提高代码质量,并且在写单元测试时发现功能代码间的依赖等设计问题,从而提高产品的可扩展性。
为什么需要单元测试?
当编写项目的时刻,如果我们假设底层的代码是正确无误的,那么先是高层代码中使用了底层代码;然后这些高层代码又被更高层的代码所使用,如此往复。当基本的底层代码不再可靠时,那么必需的改动就无法只局限在底层。虽然你可以修正底层的问题,但是这些对底层代码的修改必然会影响到高层代码。于是,一个对底层代码的修正,可能会导致对几乎所有代码的一连串改动,从而使修改越来越多,也越来越复杂。从而使整个项目也以失败告终。
而单元测试的核心内涵:这个简单有效的技术就是为了令代码变得更加完美。
编写优秀的单元测试的好处
单元测试集中注意力于程序的基本组成部分,首先保证每个单元测试通过,才能使下一步把单元组装成部件并测试其正确性具有基础。单元是整个软件的构成基础,像硬件系统中的零部件一样,只有保证零部件的质量,这个设备的质量才有基础,单元的质量也是整个软件质量的基础。因此,单元测试的效果会直接影响软件的后期测试,最终在很大程度上影响到产品的质量。
单元测试可以平行开展,这样可以使多人同时测试多个单元,提高了测试的效率。
单元规模较小,复杂性较低,因而发现错误后容易隔离和定位,有利于调试工作。
单元的规模和复杂性特点,使单元测试中可以使用包括白盒测试的覆盖分析在内的许多测试技术,能够进行比较充分细致的测试,是整个程序测试满足语句覆盖和分支覆盖要求的基础。
单元测试的测试效果是最显而易见的。做好单元测试,不仅后期的系统集成联调或集成测试和系统测试会很顺利,节约很多时间;而且在单元测试过程中能发现一些很深层次的问题,同时还会发现一些很容易发现而在集成测试和系统测试很难发现的问题;更重要的是单元测试不仅仅是证明这些代码做了什么,是如何做的,而且证明是否做了它该做的事情而没有做不该做的事情。
单元测试的好与坏不仅直接关系到测试成本(因为如果单元测试中易发现的问题拖到后期测试发现,那么其成本将成倍数上升),而且也会直接影响到产品质量,因为可能就是由于代码中的某一个小错误就导致了整个产品的质量降低一个指标,或者导致更严重的后果。
单元测试帮助设计
单元测试迫使我们从关注实现转向关注接口,编写单元测试的过程就是设计接口的过程,使单元测试通过的过程是我们编写实现的过程。我一直觉得这是单元测试最重要的好处,让我们关注的重点放在接口上而非实现的细节。单元测试帮助编码
应用单元测试会使我们主动消除和减少不必要的耦合,虽然出发点可能是为了更方便的完成单元测试,但结果通常是类型的职责更加内聚,类型间的耦合显著降低。这是已知的提升编码质量的有效手段,也是提升开发人员编码水平的有效手段。单元测试帮助重构
对于现有项目的重构,从编写单元测试开始是更好的选择。先从局部代码进行重构,提取接口进行单元测试,然后再进行类型和层次级别的重构。
事实上,单元测试是一种验证行为—— 测试和验证程序中每一项功能的正确性,为以后的开发提供支持;单元测试是一种设计行为—— 编写单元测试将使我们从调用者观察、思考,特别是要先考虑测试,这样就可把程序设计成易于调用和可测试的,并努力降低软件中的耦合,还可以使编码人员在编码时产生预测试,将程序的缺陷降低到最小;单元测试是一种编写文档的行为—— 是展示函数或类如何使用的最佳文档;单元测试具有回归性—— 自动化的单元测试有助于进行回归测试。
单元测试在设计、编码和调试上的作用足以使其成为软件开发相关人员的必备技能。
断言(Assert)
断言表示为一些布尔表达式,程序员相信在程序中的某个特定点该表达式值为真,可以在任何时候启用和禁用断言验证,因此可以在测试时启用断言而在部署时禁用断言。同样,程序投入运行后,最终用户在遇到问题时可以重新启用断言。
使用断言可以创建更稳定、品质更好且 不易于出错的代码。当需要在一个值为FALSE时中断当前操作的话,可以使用断言。单元测试必须使用断言(Xunit/Nunit)。
使用伪对象
伪对象可以解决要测试的代码中使用了无法测试的外部依赖问题,更重要的是通过接口抽象实现了低耦合。例如通过抽象IConfigurationManager接口来使用ConfigurationManager对象,看起来似乎只是为了单元测试而增加更多的代码,实际上我们通常不关心后去的配置是否是通过ConfigurationManager静态类读取的config文件,我们只关心配置的取值,此时使用IConfigurationManager既可以不依赖具体的ConfigurationManager类型,又可以在系统需要扩展时使用其他实现了IConfigurationManager接口的实现类。
使用伪对象解决外部依赖的主要步骤:
使用接口依赖取代原始类型依赖。
通过对原始类型的适配实现上述接口。
手动创建用于单元测试的接口实现类或在单元测试时使用Mock框架生成接口的实例。
手动创建的实现类完整的实现了接口,这样的实现类可以在多个测试中使用。可以选择使用Mock框架生成对应接口的实例,只需要对当前测试需要调用的方法进行模拟,通常需要根据参数进行逻辑判断,返回不同的结果。无论是手动实现的模拟类对象还是Mock生成的伪对象都称为桩对象,即Stub对象。Stub对象的本质是被测试类依赖接口的伪对象,它保证了被测试类可以被测试代码正常调用。
解决了被测试类的依赖问题,还需要解决无法直接在被测试方法上使用Assert断言的情况。此时我们需要在另一类伪对象上使用Assert,通常我们把Assert使用的模拟对象称为模拟对象,即Mock对象。Mock对象的本质是用来提供给Assert进行验证的,它保证了在无法直接使用断言时可以正常验证被测试类。
Stub和Mock对象都是伪对象,即Fake对象。
Stub或Mock对象的区分明白了就很简单,从被测试类的角度讲Stub对象,从Assert的角度讲Mock对象。然而,即使不了解相关的含义和区别也不会在使用时产生问题。比如测试邮件发送,我们通常不能直接在被测试代码上应用Assert,我们会在模拟的STMP服务器对象上应用Assert判断是否成功接收到邮件,这个SMTPServer模拟对象就是Mock对象而不是Stub对象。比如写日志,我们通常可以直接在ILogger接口的相关方法上应用Assert判断是否成功,此时的Logger对象即是Stub对象也是Mock对象。
.NET单元测试常用框架和组件
XUnit
XUnit是目前最为流行的.NET单元测试框架。NUnit出现的较早被广泛使用,如nopCommerce、Orchard等项目从开始就一直使用的是NUnit。XUnit目前是比NUnit更好的选择,从github上可以看到asp.net mvc等一系列的微软项目使用的就是XUnit框架。
xUnit是各种代码驱动测试框架的统称,可以测试软件的不同单元。xUnit的特点是:提供了一个自动化测试3的解决方案,无须多次编写重复的测试代码,也无须记住该测试的预期结果。
四要素:
- 测试Fixtures
Fixture指被测试的目标。而测试Fixture是一组单元测试成功的预定条件或预期结果的设定。
- 测试集
测试集是一组测试用例。但同一组内的测试用例必须有相同的测试Fixture。
- 测试执行
单个的单元测试的执行需要按照一定的方式进行。
- 断言
断言是验证被测试的程序在测试中的行为或状态的一个宏4或函数。若断言失败,则代表引发异常,终止测试的继续执行。
NUnit
NUnit作为xUnit家族中的.Net成员,是.NET的单元测试框架,xUnit是一套适合于多种语言的单元测试工具。它具有如下特征:
- 提供了API,使得我们可以创建一个带有“通过/失败”结果的重复单元。
- 包括了运行测试和表示结果所需的工具。
- 允许多个测试作为一个组在一个批处理中运行。
- 非常灵巧,操作简单,我们花费很少的时间即可学会并且不会给测试的程序添加额外的负担。
功能可以扩展,如果希望更多的功能,可以很容易的扩展它。
官方主页:http://www.NUnit.org
MSTest
MS Test框架是Visual Studio自带的测试框架,可以通过新建一个Unit Test Project工程,也可以建一个Class Libary,然后添加对Microsoft.VisualStudio.QualityTools.UnitTestFramework.dll的引用。然后就是创建测试用例,进行测试即可。其主要特点是与Visual Studio完美集成。
MSTest、NUnit、xUnit.net 属性对照表
MSTest | NUnit | xUnit.net | Comments |
---|---|---|---|
[TestMethod] | [Test] | [Fact] | Marks a test method. |
[TestClass] | [TestFixture] | n/a | xUnit.net does not require an attribute for a test class; it looks for all test methods in all public (exported) classes in the assembly. |
[ExpectedException] | [ExpectedException] | Assert.Throws Record.Exception | xUnit.net has done away with the ExpectedException attribute in favor of Assert.Throws. |
[TestInitialize] | [SetUp] | Constructor | We believe that use of [SetUp]is generally bad. However, you can implement a parameterless constructor as a direct replacement. |
[TestCleanup] | [TearDown] | IDisposable.Dispose | We believe that use of[TearDown] is generally bad. However, you can implementIDisposable.Dispose as a direct replacement. |
[ClassInitialize] | [TestFixtureSetUp] | IUseFixture<T> | To get per-fixture setup, implement IUseFixture<T> on your test class. |
[ClassCleanup] | [TestFixtureTearDown] | IUseFixture<T> | To get per-fixture teardown, implement IUseFixture<T> on your test class. |
[Ignore] | [Ignore] | [Fact(Skip=”reason”)] | Set the Skip parameter on the[Fact] attribute to temporarily skip a test. |
[Timeout] | [Timeout] | [Fact(Timeout=n)] | Set the Timeout parameter on the [Fact] attribute to cause a test to fail if it takes too long to run. Note that the timeout value for xUnit.net is in milliseconds. |
[TestCategory] | [Category] | [Trait] | |
[TestProperty] | [Property] | [Trait] | Set arbitrary metadata on a test |
[DataSource] | n/a | [Theory], [XxxData] | Theory (data-driven test). |
MSTest、NUnit、xUnit.net 断言对照表
MSTest | NUnit | xUnit.net | Comments |
---|---|---|---|
AreEqual | AreEqual | Equal | MSTest and xUnit.net support generic versions of this method. |
AreNotEqual | AreNotEqual | NotEqual | MSTest and xUnit.net support generic versions of this method. |
AreNotSame | AreNotSame | NotSame | |
AreSame | AreSame | Same | |
Contains (on CollectionAssert) | Contains | Contains | |
n/a | DoAssert | n/a | |
DoesNotContain (on CollectionAssert) | n/a | DoesNotContain | |
n/a | n/a | DoesNotThrow | Ensures that the code does not throw any exceptions |
Fail | Fail | n/a | xUnit.net alternative: Assert.True(false, “message”) |
n/a | Pass | n/a | |
n/a | Greater | n/a | xUnit.net alternative: Assert.True(x > y) |
n/a | GreaterOrEqual | n/a | |
Inconclusive | Ignore | n/a | |
n/a | n/a | InRange | Ensures that a value is in a given inclusive range (note: NUnit and MSTest have limited support for InRange on their AreEqual methods) |
n/a | IsAssignableFrom | IsAssignableFrom | |
n/a | IsEmpty | Empty | |
IsFalse | IsFalse | False | |
IsInstanceOfType | IsInstanceOfType | IsType | |
n/a | IsNaN | n/a | xUnit.net alternative: Assert.True(double.IsNaN(x)) |
n/a | IsNotAssignableFrom | n/a | xUnit.net alternative: Assert.False(obj is Type); |
n/a | IsNotEmpty | NotEmpty | |
IsNotInstanceOfType | IsNotInstanceOfType | IsNotType | |
IsNotNull | IsNotNull | NotNull | |
IsNull | IsNull | Null | |
IsTrue | IsTrue | True | |
n/a | Less | n/a | xUnit.net alternative: Assert.True(x < y) |
n/a | LessOrEqual | n/a | |
n/a | n/a | NotInRange | Ensures that a value is not in a given inclusive range |
n/a | Throws | Throws | Ensures that the code throws an exact exception |
n/a | IsAssignableFrom | n/a | |
n/a | IsNotAssignableFrom | n/a |