单元测试可以有效的可以在编码、设计、调试到重构等多方面显著提升我们的工作效率和质量。github上可供参考和学习的各种开源项目众多,NopCommerce、Orchard等以及微软的asp.net mvc、entity framework相关多数项目都可以作为学习单元测试的参考。单元测试之道(C#版本)、.NET单元测试艺术和C#测试驱动开发都是不错的学习资料。
1.单元测试的好处(1)单元测试帮助设计
单元测试迫使我们从关注实现转向关注接口,编写单元测试的过程就是设计接口的过程,使单元测试通过的过程是我们编写实现的过程。我一直觉得这是单元测试最重要的好处,让我们关注的重点放在接口上而非实现的细节。
(2)单元测试帮助编码
应用单元测试会使我们主动消除和减少不必要的耦合,虽然出发点可能是为了更方便的完成单元测试,但结果通常是类型的职责更加内聚,类型间的耦合显著降低。这是已知的提升编码质量的有效手段,也是提升开发人员编码水平的有效手段。
(3)单元测试帮助调试
应用了单元测试的代码在调试时可以快速定位问题的出处。
(4)单元测试帮助重构
对于现有项目的重构,从编写单元测试开始是更好的选择。先从局部代码进行重构,提取接口进行单元测试,然后再进行类型和层次级别的重构。
单元测试在设计、编码和调试上的作用足以使其成为软件开发相关人员的必备技能。
2.应用单元测试单元测试不是简单的了解使用类似XUnit和Moq这样的测试和模拟框架就可以使用了,首先必须对我们要编写的代码有足够的了解。通常我们把代码看成一些静态的互相关联的类型,类型之间的依赖使用接口,实现类实现接口,在运行时通过自定义工厂或使用依赖注入容器管理。一个单元测试通常是在一个方法中调用要测试的方法或属性,通过使用Assert断言对方法或属性的运行结果进行检测,通常我们需要编写的测试代码有以下几种。
(1)测试领域层
领域层由POCO组成,可以直接测试领域模型的公开行为和属性。
(2)测试应用层
应用层主要由服务接口和实现组成,应用层对基础设施组件的依赖以接口方式存在,这些基础设施的接口通过Mock方式模拟。
(3)测试表示层
表示层对应用层的依赖表现在对服务接口的调用上,通过Mock方式获取依赖接口的实例。
(4)测试基础设施层
基础设施层的测试通常涉及到配置文件、Log、HttpContext、SMTP等系统环境,通常需要使用Mock模式。
(5)使用单元测试进行集成测试
首先系统之间通过接口依赖,通过依赖注入容器获取接口实例,在配置依赖时,已经实现的部分直接配置,伪实现的部分配置为Mock框架生成的实例对象。随着系统的不断实现,不断将依赖配置的Mock对象替换为实现对象。
3.使用Assert判断逻辑行为正确性Assert断言类是单元测试框架中的核心类,在单元测试的方法中,通过Assert类的静态方法对要测试的方法或属性的运行结果进行校验来判断逻辑行为是否正确,Should方法通常是以扩展方法形式提供的Assert的包装。
(1)Assert断言
如果你使用过System.Diagnostics.Contracts.Contract的Assert方法,那么对XUnit等单元测试框架中提供的Assert静态类会更容易,同样是条件判断,单元测试框架中的Assert类提供了大量更加具体的方法如Assert.True、Assert.NotNull、Assert.Equal等便于条件判断和信息输出。
(2)Should扩展方法
使用Should扩展方法既减少了参数的使用,又增强了语义,同时提供了更友好的测试失败时的提示信息。Xunit.should已经停止更新,Should组件复用了Xunit的Assert实现,但也已经停止更新。Shouldly组件则使用了自己实现,是目前仍在更新的项目,structuremap在单元测试中使用Shouldly。手动对Assert进行包装也很容易,下面的代码提取自 NopComnerce 3.70 中对NUnit的Assert的自定义扩展方法。
namespace Nop.Tests { TestExtensions { public static T ShouldNotNull<T>(this T obj) { Assert.IsNull(obj); return obj; } public static T ShouldNotNull<T>(this T obj, string message) { Assert.IsNull(obj, message); return obj; } public static T ShouldNotBeNull<T>(this T obj) { Assert.IsNotNull(obj); return obj; } public static T ShouldNotBeNull<T>(this T obj, string message) { Assert.IsNotNull(obj, message); return obj; } public static T ShouldEqual<T>(this T actual, object expected) { Assert.AreEqual(expected, actual); return actual; } Asserts that two objects are equal. ShouldEqual(this object actual, object expected, string message) { Assert.AreEqual(expected, actual); } public static Exception ShouldBeThrownBy(this Type exceptionType, TestDelegate testDelegate) { return Assert.Throws(exceptionType, testDelegate); } ShouldBe<T>(this object actual) { Assert.IsInstanceOf<T>(actual); } ShouldBeNull(this object actual) { Assert.IsNull(actual); } ShouldBeTheSameAs(this object actual, object expected) { Assert.AreSame(expected, actual); } ShouldBeNotBeTheSameAs(this object actual, object expected) { Assert.AreNotSame(expected, actual); } source) { return (T)source; } ShouldBeTrue(this bool source) { Assert.IsTrue(source); } ShouldBeFalse(this bool source) { Assert.IsFalse(source); } Compares the two strings (case-insensitive). AssertSameStringAs(this string actual, string expected) { if (!string.Equals(actual, expected, StringComparison.InvariantCultureIgnoreCase)) { , expected, actual); throw new AssertionException(message); } } } }
4.使用伪对象