单元测试可以有效的可以在编码、设计、调试到重构等多方面显著提升我们的工作效率和质量。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
{
public static class 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;
} ///<summary>
/// Asserts that two objects are equal.
///</summary>
///<param name="actual"></param>
///<param name="expected"></param>
///<param name="message"></param>
///<exception cref="AssertionException"></exception>
public static void 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);
} public static void ShouldBe<T>(this object actual)
{
Assert.IsInstanceOf<T>(actual);
} public static void ShouldBeNull(this object actual)
{
Assert.IsNull(actual);
} public static void ShouldBeTheSameAs(this object actual, object expected)
{
Assert.AreSame(expected, actual);
} public static void ShouldBeNotBeTheSameAs(this object actual, object expected)
{
Assert.AreNotSame(expected, actual);
} public static T CastTo<T>(this object source)
{
return (T)source;
} public static void ShouldBeTrue(this bool source)
{
Assert.IsTrue(source);
} public static void ShouldBeFalse(this bool source)
{
Assert.IsFalse(source);
} /// <summary>
/// Compares the two strings (case-insensitive).
/// </summary>
/// <param name="actual"></param>
/// <param name="expected"></param>
public static void AssertSameStringAs(this string actual, string expected)
{
if (!string.Equals(actual, expected, StringComparison.InvariantCultureIgnoreCase))
{
var message = string.Format("Expected {0} but was {1}", expected, actual);
throw new AssertionException(message);
}
}
}
}

4.使用伪对象

伪对象可以解决要测试的代码中使用了无法测试的外部依赖问题,更重要的是通过接口抽象实现了低耦合。例如通过抽象IConfigurationManager接口来使用ConfigurationManager对象,看起来似乎只是为了单元测试而增加更多的代码,实际上我们通常不关心后去的配置是否是通过ConfigurationManager静态类读取的config文件,我们只关心配置的取值,此时使用IConfigurationManager既可以不依赖具体的ConfigurationManager类型,又可以在系统需要扩展时使用其他实现了IConfigurationManager接口的实现类。

使用伪对象解决外部依赖的主要步骤:

(1)使用接口依赖取代原始类型依赖。

(2)通过对原始类型的适配实现上述接口。

(3)手动创建用于单元测试的接口实现类或在单元测试时使用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对象。

5.单元测试常用框架和组件

(1)单元测试框架。

XUnit是目前最为流行的.NET单元测试框架。NUnit出现的较早被广泛使用,如nopCommerce、Orchard等项目从开始就一直使用的是NUnit。XUnit目前是比NUnit更好的选择,从github上可以看到asp.net mvc等一系列的微软项目使用的就是XUnit框架。

(2)Mock框架

Moq是目前最为流行的Mock框架。Orchard、asp.net mvc等微软项目使用Moq。nopCommerce使用Rhino Mocks。NSubstitute和FakeItEasy是其他两种应用广泛的Mock框架。

(3)邮件发送的Mock组件netDumbster

可以通过nuget获取netDumbster组件,该组件提供了SimpleSmtpServer对象用于模拟邮件发送环境。

通常我们无法直接对邮件发送使用Assert,使用netDumbster我们可以对模拟服务器接收的邮件应用Assert。

public void SendMailTest()
{
SimpleSmtpServer server = SimpleSmtpServer.Start();
IEmailSender sender = new SMTPAdapter();
sender.SendMail("sender@here.com", "receiver@there.com", "subject", "body");
Assert.Equal(, server.ReceivedEmailCount);
SmtpMessage mail = (SmtpMessage)server.ReceivedEmail[];
Assert.Equal("sender@here.com", mail.Headers["From"]);
Assert.Equal("receiver@there.com", mail.Headers["To"]);
Assert.Equal("subject", mail.Headers["Subject"]);
Assert.Equal("body", mail.MessageParts[].BodyData);
server.Stop();
}

(4)HttpContext的Mock组件HttpSimulator

同样可以通过nuget获取,通过使用HttpSimulator对象发起Http请求,在其生命周期内HttContext对象为可用状态。

由于HttpContext是封闭的无法使用Moq模拟,通常我们使用如下代码片断:

private HttpContext SetHttpContext()
{
HttpRequest httpRequest = new HttpRequest("", "http://mySomething/", "");
StringWriter stringWriter = new StringWriter();
HttpResponse httpResponse = new HttpResponse(stringWriter);
HttpContext httpContextMock = new HttpContext(httpRequest, httpResponse);
HttpContext.Current = httpContextMock;
return HttpContext.Current;
}

使用HttpSimulator后我们可以简化代码为:

using (HttpSimulator simulator = new HttpSimulator())
{ }

这对使用IoC容器和EntityFramework的程序的DbContext生命周期的测试十分重要,DbContext的生命周期必须和HttpRequest一致,因此对IoC容器进行生命周期的测试是必须的。

6.使用单元测试的难处

(1)不愿意付出学习成本和改变现有开发习惯。

(2)没有思考的习惯,错误的把单元测试当框架学。

(3)在项目后期才应用单元测试,即获取不到单元测试的好处又因为代码的测试不友好对单元测试产生误解。

(4)拒绝考虑效率、扩展性和解耦,只考虑数据和功能的实现。

ASP.NET 系列:单元测试的更多相关文章

  1. 【ASP.NET系列】详解Views

    描述 本片文章内容属于ASP.NET MVC系列视图篇,主要讲解View,大致内容如下: 1.Views文件夹讲解 2.View种类 3.Razor语法 4.对视图的基本操作 一   Views文件夹 ...

  2. 浅谈ASP.NET ---- 系列文章

    [01]浅谈Google Chrome浏览器(理论篇) [02]浅谈Google Chrome浏览器(操作篇)(上) [03]浅谈Google Chrome浏览器(操作篇)(下) [04]浅谈ASP. ...

  3. 【转】.NET(C#):浅谈程序集清单资源和RESX资源 关于单元测试的思考--Asp.Net Core单元测试最佳实践 封装自己的dapper lambda扩展-设计篇 编写自己的dapper lambda扩展-使用篇 正确理解CAP定理 Quartz.NET的使用(附源码) 整理自己的.net工具库 GC的前世与今生 Visual Studio Package 插件开发之自动生

    [转].NET(C#):浅谈程序集清单资源和RESX资源   目录 程序集清单资源 RESX资源文件 使用ResourceReader和ResourceSet解析二进制资源文件 使用ResourceM ...

  4. Asp.Net Core 单元测试正确姿势

    背景 ASP.NET Core 支持依赖关系注入 (DI) 软件设计模式,并且默认注入了很多服务,具体可以参考 官方文档, 相信只要使用过依赖注入框架的同学,都会对此有不同深入的理解,在此无需赘言. ...

  5. ASP.NET 5 单元测试中使用依赖注入

    相关博文:<ASP.NET 5 使用 TestServer 进行单元测试> 在上一篇博文中,主要说的是,使用 TestServer 对 ASP.NET 5 WebApi 进行单元测试,依赖 ...

  6. ASP.NET 系列:单元测试之StructureMap

    ASP.NET使用StructureMap等依赖注入组件时最重要就是EntityFramework的DbContext对象要保证在每次HttpRequest只有一个DbContext实例,这里将使用第 ...

  7. ASP.NET 系列:单元测试之Log4Net

    使用Log组件时,我们通常自定义ILogger接口,使用Log4Net等组件进行适配来定义不同的实现类.使用Log4Net日志组件时,为了即方便单元测试又能使用配置文件,我们通过Log4Net的ILo ...

  8. Asp.net WebAPI 单元测试

    现在Asp.net webapi 运用的越来越多,其单元而是也越来越重要.一般软件开发都是多层结构,上层调用下层的接口,而各层的实现人员不同,一般大家都只写自己对应单元测试.对下层的依赖我们通过IOC ...

  9. 一点一点学ASP.NET系列

    转自:http://www.cnblogs.com/stwyhm/archive/2006/08/10/473075.html 做开发近两年了,自认为自己还算是个知道要上进的人,每天不停地学习,不停地 ...

随机推荐

  1. DateTime的精度小问题

    一般来说判断时间的话,用个DateTime类型就已经够用了.但是有些情况,比如下面这种 DECLARE @DT1 DATETIME DECLARE @DT2 DATETIME SELECT @DT1 ...

  2. 查看Android支持的硬解码信息

    通过/system/etc/media_codecs.xml可以确定当前设备支持哪些硬解码.通过/system/etc/media_profiles.xml可以知道设备支持的具体profile和lev ...

  3. 创建docker镜像,初始化jdk8与tomcat环境

    一.创建Dockerfile文件: 创建Dockerfile文件,下载jdk与tomcat放在Dockerfile同目录下. Dockerfile文件内容: FROM Ubuntu:14.10 MAI ...

  4. redis安装及基础操作(1)

    ============================================================= 编译安装 0.环境 Linux:centos6.5 redis:3.0.5 ...

  5. shell脚本学习指南

    一.UNIX各个工具所支持的正则表达式: 二.使用sed在某一目录下建立一份目录备份: find /home/foo/ -type d -print | #找出/home/foo/目录下所有目录文件 ...

  6. Book LIst

    Go ahead. Linux APUE Linux Kernel Development 鸟哥的linux私房菜 基础篇 鸟哥的linux私房菜 服务器篇 Network Computer Netw ...

  7. Vim 命令整理

    1. 文件命令 2. 模式切换 3. 移动命令 4. 书签命令 5. 修改命令 6. 可视化操作 7. 区域选择 8. 宏命令 9. 分屏 10. 系统设置命令 1. 文件命令 [:]开始的命令需要输 ...

  8. 【转】selenium学习路线

    selenium学习路线 配置你的测试环境,真对你所学习语言,来配置你相应的selenium 测试环境.selenium 好比定义的语义---“问好”,假如你使用的是中文,为了表术问好,你的写法是“你 ...

  9. 给深度学习入门者的Python快速教程 - 基础篇

    实在搞不定博客园的排版,排版更佳的版本在: https://zhuanlan.zhihu.com/p/24162430 Life is short, you need Python 人生苦短,我用Py ...

  10. shiro和quartz同时存在于项目中,解决冲突的方案

    shiro自带了quartz定时任务,不过版本是1.3的 很多项目都会使用shiro,另外定时任务也会使用,quartz的版本2.2目前和shiro不兼容 有人通过修改源码可以解决 我这边是这样解决的 ...