ASP.NET 系列:单元测试
单元测试可以有效的可以在编码、设计、调试到重构等多方面显著提升我们的工作效率和质量。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 系列:单元测试的更多相关文章
- 【ASP.NET系列】详解Views
描述 本片文章内容属于ASP.NET MVC系列视图篇,主要讲解View,大致内容如下: 1.Views文件夹讲解 2.View种类 3.Razor语法 4.对视图的基本操作 一 Views文件夹 ...
- 浅谈ASP.NET ---- 系列文章
[01]浅谈Google Chrome浏览器(理论篇) [02]浅谈Google Chrome浏览器(操作篇)(上) [03]浅谈Google Chrome浏览器(操作篇)(下) [04]浅谈ASP. ...
- 【转】.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 ...
- Asp.Net Core 单元测试正确姿势
背景 ASP.NET Core 支持依赖关系注入 (DI) 软件设计模式,并且默认注入了很多服务,具体可以参考 官方文档, 相信只要使用过依赖注入框架的同学,都会对此有不同深入的理解,在此无需赘言. ...
- ASP.NET 5 单元测试中使用依赖注入
相关博文:<ASP.NET 5 使用 TestServer 进行单元测试> 在上一篇博文中,主要说的是,使用 TestServer 对 ASP.NET 5 WebApi 进行单元测试,依赖 ...
- ASP.NET 系列:单元测试之StructureMap
ASP.NET使用StructureMap等依赖注入组件时最重要就是EntityFramework的DbContext对象要保证在每次HttpRequest只有一个DbContext实例,这里将使用第 ...
- ASP.NET 系列:单元测试之Log4Net
使用Log组件时,我们通常自定义ILogger接口,使用Log4Net等组件进行适配来定义不同的实现类.使用Log4Net日志组件时,为了即方便单元测试又能使用配置文件,我们通过Log4Net的ILo ...
- Asp.net WebAPI 单元测试
现在Asp.net webapi 运用的越来越多,其单元而是也越来越重要.一般软件开发都是多层结构,上层调用下层的接口,而各层的实现人员不同,一般大家都只写自己对应单元测试.对下层的依赖我们通过IOC ...
- 一点一点学ASP.NET系列
转自:http://www.cnblogs.com/stwyhm/archive/2006/08/10/473075.html 做开发近两年了,自认为自己还算是个知道要上进的人,每天不停地学习,不停地 ...
随机推荐
- 使用Source Safe for SQL Server解决数据库版本管理问题(转载)
简介 在软件开发过程中,版本控制是一个广为人知的概念.因为一个项目可能会需要不同角色人员的参与,通过使用版本控制软件,可以使得项目中不同角色的人并行参与到项目当中.源代码控制使得代码可以存在多个版本, ...
- Linux[Fedora]查找文件包含的字段
find 与 grep组合查找 find . –name '文件类型' | xargs grep –n '查找内容'文件类型可正则表达式通配, [.]表示当前目录下进行查找,也可自由指定目录.比如: ...
- 虚拟机centos6.5 --开放端口
系统:centos6.5 1.查看端口开放情况 /etc/init.d/iptables status 2.开启端口 /sbin/iptables -I INPUT -p tcp --dport -j ...
- TFS 2013 生成(构建)历史记录保持策略(Retention Policy)
TFS服务器通过自动构建,实现软件生成和发布的自动化过程,这一直是TFS系统中非常重要的一个功能模块.近年来发布的TFS版本,都在自动化构建方面大幅增强了相应的功能.在这篇博客里我主要总结TFS 20 ...
- Sql server2012连接Sql server 2008时出现的问题:已成功与服务器建立连接,但在登陆过程中发生错误。(provider:SSL Provider,error:0-接收到的消息异常,或格式不正确。)
以前连接是正常的,就这两天连不上了.(没有耐心的直接看末尾解决办法) 错误消息如下: 1.尝试读取或写入受保护的内存.这通常指示其他内存已损坏.(System.Data) 2.已成功与服务器建立连接, ...
- 大话设计模式C++版——工厂模式在COM中的典型应用
上篇<大话设计模式C++版——抽象工厂模式>中,我们拯救世界未遂,留下小小的遗憾,本篇中我们将给出一个解决方案——COM组件技术,同时也顺便扯扯工厂模式在COM组件技术中的应用. 工厂模式 ...
- 广州APP开发外包公司哪家比较好?广州达到信息技术有限公司技术到底怎么样?
广州APP开发公司哪家比较好,广州手机APP软件开发公司广州达到信息表示:用户的刚性需求是公司使用手机APP软件盈利的根本前提和基础,所以开发一款手机APP应用时必须从客户的角度来思考.因此公 ...
- 第18章 图元文件_18.2 增强型图元文件(emf)(1)
18.2 增强型图元文件(emf) 18.2.1 创建并显示增强型图元文件的步骤 (1)创建:hdcEMF = CreateEnhMetaFile(hdcRef,szFilename,lpRect,l ...
- JavaScript语言精粹笔记
JavaScript语言精粹笔记 掌握语言的每个特性可以让你出风头,但是并不推荐,因为一部分的特性带来的麻烦可能远超本身的价值.正如书中所言,坏的材料并不能雕刻出好的作品,要成为一名更好的程序员,要取 ...
- 更改项目名或者多个项目时,发现多个"Home"匹配的Controller时,解决方法
[备份]异常信息:找到多个与名为“Home”的控制器匹配的类型.如果为此请求(“{controller}/{action}/{id}”)提供服务的 路由在搜索匹配此请求的控制器时没有指定命名空间,则会 ...