ASP.NET MVC之单元测试分分钟的事
一、为什么要进行单元测试?
大部分开发者都有个习惯(包括本人在内),常常不喜欢去做单元测试。因为我们对自己写的程序总是盲目自信,或者存在侥幸心理每次运行通过后就直接扔给测试组的妹子们了。结果妹子一测,大把大把的bug出现了,最后每每看到测试的妹子走过来,心里就只想说一句话:你是猴子请来的逗比吗?本来想节省时间,结果最后花在找BUG和修复BUG的这些时间加起来已经比开发这个模块所花的时间还要多了,最后更要命的是,坑爹的加班就在所难免了!如果一开始将bug遏制在萌芽状态,我们至于这么苦逼吗?SO,单元测试很有必要!
二、单元测试法则
1、单元测试必须能够重复执行,就是能够非常频繁地执行
2、单元测试的执行速度不能太慢,要不然会影响开发进度的
3、单元测试不应该依赖于外部资源和真实的环境
4、单元测试不应该涉及到真实数据库的操作
5、要确保单元测试的可信度
6、单元测试通常以测试一个方法为单位
7、每一个程序猿都需要为自己写的代码编写单元测试代码
三、单元测试工具
我在这里仅仅推荐一个比较实用的测试工具NUnit,可单独使用,也可以通过TestDriven.NET(TestDriven.NET是以插件形式集成在Visual Studio IDE中的单元测试工具,完全兼容所有.NET Framework版本,并且集成了多种单元测试框架诸如NUnit,MbUnit,以及 MS Team System 等)将其加入到vs中。
NUnit作为xUnit家族中的.Net成员,是.NET的单元测试框架,xUnit是一套适合于多种语言的单元测试工具。它具有如下特征:
- 提供了API,使得我们可以创建一个带有“通过/失败”结果的重复单元。
- 包括了运行测试和表示结果所需的工具。
- 允许多个测试作为一个组在一个批处理中运行。
- 非常灵巧,操作简单,我们花费很少的时间即可学会并且不会给测试的程序添加额外的负担。
- 功能可以扩展,如果希望更多的功能,可以很容易的扩展它。
套用老罗的话就是一句话:它是当今.NET领域最牛逼的测试工具之一
在.NET下的单元测试工具其实非常多,这里不想多说,我们就使用微软自己提供的测试框架Unit Test Framework,已经集成在vs中了~
四、MOQ
单元测试的目标是一次只测试一个方法,是一种细粒度的测试,但是假如某个方法依赖于其他一些难以操控的外部东东,比如说网络连接、数据库连接等时,那么我们该怎么办呢?既然单元测试的法则说不让依赖这些个外部真实的东西,那还不简单,我山寨一个不就行了吗?此时当采用以假乱真的手法来完成单元测试。实际上我们这里采用的是Mock对象,也就是真实对象的替代品,并使用Moq框架来模拟Mock对象,它为我们提供了模拟真实对象行为的能力,然后交给被测试功能使用,以此判断被测试功能是否正确。
注意:Moq只能模拟接口或抽象类。
你可以通过Nuget来获取Moq并且引用到指定的项目,也可以在google上下载,不管怎样记得在测试项目中引用Moq.dll就行~
举个栗子:
public class Student
{
public string ID { get; set; }
public string Name { get; set; }
public int Age { get; set; }
}
IStudentRepository
public interface IStudentRepository
{
Student GetStudentById(string id);
}
下面是方法GetStudentById的单元测试代码:
[TestMethod]
public void GetStudentByIdTest()
{
//创建MOCK对象
var mock = new Mock<IStudentRepository>();
//设置MOCK调用行为
mock.Setup(p=>p.GetStudentById("")).Returns(new Student());
//MOCK调用方法
mock.Object.GetStudentById("");
Assert.AreNotSame(new Student(), mock.Object.GetStudentById(""));
}
这里其实已经以假乱真了,因为真实的接口IStudentRepository里边的方法GetStudentById在实际中肯定要要访问数据库的,那么我们这里压根都么有访问什么数据库,直接用IStudentRepository接口模拟了一个对象,根本不用实现,这样瞬间就提高了单元测试的可行性。不过这里也要提个醒,就是在写代码的时候,别让代码产生过度的依赖,方可在进行单元测试时顺利进行!
说说常用的Moq成员
1、Mock<T>:通过这个类我们能够得到一个Mock<T>对象,T可以是接口和类。它有一个公开的Object属性,这个就是我们Moq为我们模拟出的对象。
var mo = new Mock<IStudentRepository>();
mo.Object //其实就是模拟实现IStudentRepository接口的对象
2、It:这是一个静态类,用于过滤参数。
It很适合用来匹配数字,字符串参数,它提供了如下几个静态方法:
Is<TValue> :参数为Expression<Predict<TValue>>类型,当你需要某种类型并且这种类型要通过代码来判断的话可以使用它。
IsAny<TValue> :没有参数,只要是TValue类型的就能匹配成功。
IsInRange<TValue> :用来匹配两个的TValue类型值之间的参数。(Range参数可以设定开闭区间)
IsRegex:用正则表达式匹配。(仅限于字符串类型参数)
var customer = new Mock<ICustomer>();
customer.Setup(x => x.SelfMatch(It.Is<int>(i => i % == ))).Returns("");//方法SelfMatch接受int型参数,当参数为偶数时,才返回字符串1。
customer.Setup(p => p.SelfMatch(It.IsAny<int>())).Returns((int k) => "任何数:" + k);//方法SelfMatch接受int型,且任何int型参数都可以,然后返回:"任何数:" + k。
customer.Setup(p => p.SelfMatch(It.IsInRange<int>(0, 10, Range.Inclusive))).Returns("10以内的数");//方法SelfMatch接受int型,且当范围在[0,10]时,才返回10以内的数
customer.Setup(p => p.ShowException(It.IsRegex(@"^\d+$"))).Throws(new Exception("不能是数字"));//用正则表达式过滤参数不能是数字
3、MockBehavior:用于配置MockObject的行为,比如是否自动mock。
Moq有个枚举类型MockBehavior,有三个值Strict,Loose,Default。
Strict表示Mock对象在调用一个方法前这个方法必须被Mock掉,否则就会引发MockException。而Loose与之相反,如果调用没有Mock的方法也不会出错。Default默认为Loose。例如:
[TestMethod]
public void MoqTest()
{
var mo = new Mock<ICustomer>(MockBehavior.Strict);
mo.Object.Method();//在MockBehavior.Strict设置下,一切调用未填充的方法/属性/事件时会抛出异常
}
4、MockFactory:Mock对象工厂,能够批量生产统一自定义配置的Mock对象,也能批量的进行Mock对象测试。
这是一个模拟对象的工厂,我们不可以成批Mock它们,例如:
var factory = new MockFactory(MockBehavior.Strict) { DefaultValue = DefaultValue.Mock };
// Create a mock using the factory settings
var aMock = factory.Create<IStudent>();
// Create a mock overriding the factory settings
var bMock = factory.Create<ITeacher>(MockBehavior.Loose);
// Verify all verifiable expectations on all mocks created through the factory
factory.Verify();
5、Match<T>:如果你先觉得It不够用就用Match<T>,通过它能够完全自定义规则。
还是举个栗子比较能说明问题
[TestMethod()]
public void MoqTest()
{
var mo = new Mock<IRepository>();
mo.Setup(p => p.Method(MatchHelper.ParamMatcher("wang"))).Returns("success");
Assert.AreEqual(mo.Object.("wang"), “success);
}
//此处就实现了自定义的参数匹配
public static class MatchHelper
{
public static string ParamMatcher(string name)
{
return Match<string>.Create(
p => p.Equals(name));
}
}
6、Verify和VerifyAll
用于测试mock对象的方法或属性是否被调用执行,Verify必须要先调用Verifiable()方法才能用,而VerifyAll不用这样就可以对所有的mock对象进行验证,例如:
public void TestVerify()
{
var customer = new Mock<ICustomer>();
customer.Setup(p => p.GetCall(It.IsAny<string>()))
.Returns("方法调用").Verifiable();//必须调用Verifiable()方法才可以
customer.Object.GetCall("调用了!");
customer.Verify();
}
public void TestVerifyAll()
{
var customer = new Mock<ICustomer>();
customer.Setup(p => p.GetCall(It.IsAny<string>()))
.Returns("方法调用"); //没有显式调用Verifiable()方法也可以
customer.Object.GetCall("调用了!");
customer.VerifyAll();
}
7、Callback
其实就是回调,使用Callback可以使我们在某个使用特定参数匹配的方法在被调用时得到通知。当执行某方法时,调用其内部输入的(Action)委托,例如:
public void TestCallback()
{
var customer = new Mock<ICustomer>();
customer.Setup(p => p.GetCall(It.IsAny<string>()))
.Returns("方法调用")
.Callback((string s)=>Console.WriteLine("ok"+s));
customer.Object.GetCall("x");
}
五、ASP.NET MVC单元测试应用
几点建议
1、每当你向controller、service、repository层中添加一系列的新函数时,从你开始修改代码的那一刻开始,你就必须得承担有可能破坏原本正常工作的那部分功能的风险。言外之意,你必须进行单元测试才行。
2、单元测试必须是可以快速执行的。因此对于耗时的数据库交互来说,你必须对其进行mock,然后编写代码与mock的数据库进行交互
3、你不必为view进行单元测试。因为要想对view进行测试,你就不得不搭建web服务器。因为搭建web服务器相对来说很耗时,因此并不推荐针对view进行单元测试。 如果你的view包含大量复杂的逻辑,则你应当考虑将这些逻辑转移到Helper方法中。你可以针对Helper方法编写单元测试且无需搭建web服务器。
4、对于涉及到http的东东,你也必须mock一下
如何为方法添加单元测试?
1、在新建MVC项目时为项目添加默认的单元测试项目,如图所示:
2、或者在vs中相应的方法处单击鼠标右键,添加单元测试即可,如图所示:
MVC单元测试
默认生成的单元测试代码已经为Controller生成了相应的单元测试方法,例如对HomeController进行单元测试,注意测试类的命名规范,以及两个特性TestClass和TestMethod,有了这两个东东,方可对类和方法进行测试。我们可以发现是按照arrange/act/assert的模式来进行单元测试的,单元测试说白了就是三步走:arrange:初始化测试的环境属于准备阶段;act:执行测试;assert:断言,测试的结果
[TestClass]
public class HomeControllerTest
{
[TestMethod]
public void About()
{
// Arrange
HomeController controller = new HomeController();
// Act
ViewResult result = controller.About() as ViewResult;
// Assert
Assert.IsNotNull(result);
} }
难点其实在第一步,就是测试环境的准备,这里更多的是用Moq来进行模拟。另外,涉及到的Assert类主要有以下这些方法
Assert.Inconclusive() 表示一个未验证的测试;
Assert.AreEqual() 测试指定的值是否相等,如果相等,则测试通过;
AreSame() 用于验证指定的两个对象变量是指向相同的对象,否则认为是错误
AreNotSame() 用于验证指定的两个对象变量是指向不同的对象,否则认为是错误
Assert.IsTrue() 测试指定的条件是否为True,如果为True,则测试通过;
Assert.IsFalse() 测试指定的条件是否为False,如果为False,则测试通过;
Assert.IsNull() 测试指定的对象是否为空引用,如果为空,则测试通过;
Assert.IsNotNull() 测试指定的对象是否为非空,如果不为空,则测试通过;
一个模拟访问Service服务的单元测试栗子
namespace Mvc4UnitTesting.Tests.Controllers
{
[TestClass]
public class HomeControllerTest
{
[TestMethod]
public void Index()
{
// Arrange
var mockIProductService = new Mock<IProductService>();
mockIProductService.Setup(p => p.GetAllProduct()).Returns(new List<Product> { new Product{ ProductId = , ProductName = "APPLE", Price = ""}});
HomeController controller = new HomeController(mockIProductService.Object);
// Act
ViewResult result = controller.Index() as ViewResult;
var product = (List<Product>)result.ViewData.Model;
// Assert
Assert.AreEqual("APPLE", product.First<Product>().ProductName);
}
}
}
一个模拟访问Web环境的单元测试栗子
public ActionResult Index()
{
ViewData["Message"] = Request.QueryString["WW"];
return View();
}
[TestMethod]
public void Index()
{
HomeController controller = new HomeController();
var httpContext = new Mock<HttpContextBase>();
var request=new Mock<HttpRequestBase>();
NameValueCollection queryString = new NameValueCollection();
queryString.Add("WW", "WW");
request.Setup(r => r.QueryString).Returns(queryString);
httpContext.Setup(ht => ht.Request).Returns(request.Object);
ControllerContext controllerContext = new ControllerContext();
controllerContext.HttpContext = httpContext.Object;
controller.ControllerContext = controllerContext;
ViewResult result = controller.Index() as ViewResult;
ViewDataDictionary viewData = result.ViewData;
Assert.AreEqual("WW", viewData["Message"]);
}
总结
有效的测试是软件质量的保证,所以这里希望大家,包括本人自己在内,都能够把单元测试落到实处,目前对于我们来说,最大的难点在于能否恰到好处地模拟出相关的依赖资源,因此写出低耦合的代码就变得很有必要。其实多加练习使用之后,自然就能够应对相对复杂的单元测试,终有一天你会发现,单位测试只不过是分分钟的事!
ASP.NET MVC之单元测试分分钟的事的更多相关文章
- ASP.NET MVC之单元测试
ASP.NET MVC之单元测试分分钟的事2014-07-15 13:05 by 书洞里的猫, 550 阅读, 4 评论, 收藏, 编辑 一.为什么要进行单元测试? 大部分开发者都有个习惯(包括本人在 ...
- [转]模拟HttpContext 实现ASP.NET MVC 的单元测试
众所周知 ASP.NET MVC 的一个显著优势即可以很方便的实现单元测试,但在我们测试过程中经常要用到HttpContext,而默认情况下单元测试框架是不提供HttpContext的模拟的,本文通过 ...
- ASP.NET MVC编程——单元测试
1自动化测试基本概念 自动化测试分为:单元测试,集成测试,验收测试. 单元测试 检验被测单元的功能,被测单元一般为低级别的组件,如一个类或类方法. 单元测试要满足四个条件:自治的,可重复的,独立的,快 ...
- ASP.NET MVC必须知道的那些事!
MVC的由来: 在MVC模式之前,View界面的呈现.用户交互操作的捕捉与相应.业务流程的执行以及数据的存储等都是在一起的,这种设计模式叫自治视图. 这重设计模式主要存在三大弊端: 重用性:业务逻辑与 ...
- asp.net mvc 5 单元测试小例子
using System.Collections.Generic; using System.Linq; using Microsoft.VisualStudio.TestTools.UnitTest ...
- Ninject之旅之十三:Ninject在ASP.NET MVC程序上的应用(附程序下载)
摘要: 在Windows客户端程序(WPF和Windows Forms)中使用Ninject和在控制台应用程序中使用Ninject没什么不同.在这些应用程序里我们不需要某些配置用来安装Ninject, ...
- ASP.NET MVC学习笔记(二)笔记
接下来我们一起了解ASP.NET MVC的最重要的核心技术,了解ASP.NET MVC的开发框架,生命周期,技术细节. 一.Routing与ASP.NET MVC生命周期 1.Routing——网址路 ...
- 使用IdleTest进行TDD单元测试驱动开发演练(3) 之 ASP.NET MVC
一.[前言] (1)本文将用到IOC框架Unity,可参照<Unity V3 初步使用 —— 为我的.NET项目从简单三层架构转到IOC做准备>(2)本文的解决方案是基于前述<使用I ...
- 单元测试 – ASP.NET MVC 4 系列
在开发可测试软件的过程中,单元测试已成为确保软件质量的一个不可或缺部分.测试驱动开发(Test-Driven Development,TDD)是编写单元测试的一种方法,采用该方法的开发人 ...
随机推荐
- THULAC:一个高效的中文词法分析工具包(z'z)
网址:http://thulac.thunlp.org/ THULAC(THU Lexical Analyzer for Chinese)由清华大学自然语言处理与社会人文计算实验室研制推出的一套中文词 ...
- MySQL数据库中的Date,DateTime,int,TimeStamp和Time类型的对比
DATETIME 用在你需要同时包含日期和时间信息的值时.MySQL检索并且以'YYYY-MM-DD HH:MM:SS'格式显示DATETIME值,支持的范围是'1000-01-01 00:00:00 ...
- 针对“永恒之蓝”攻击紧急处置手册(蠕虫 WannaCry)
首先确认主机是否被感染 被感染的机器屏幕会显示如下的告知付赎金的界面: 如果主机已被感染: 则将该主机隔离或断网(拔网线).若客户存在该主机备份,则启动备份恢复程序. 如果主机未被感染: 则存在四种方 ...
- java装饰设计模式
将一个对象作为参数进行传递,然后对这个对象进行包装---改变其中的方法或者添加一些新的行为---装饰设计模式---装饰者模式---Decorate Model 改变一个方法: 1. 通过继承这个类,然 ...
- Vue 不使用Vuex的情况下进行状态管理
在封装自己的Vue ui库的时候,往往要封装一些比较复杂的组件,比如说table,form之类.这些组件由于功能繁杂,还涉及到子组件嵌套及通信,如果没有一套状态管理方案的话很容易导致代码难以阅读.难以 ...
- 肖申克的救赎 -Hope
典狱长诺顿高高在上,平时道貌岸然,对圣经倒背如流,实际上攫取利益时不择手段,残酷.阴险而贪婪.狱警长海利和其他警员,凶狠残暴,充当诺顿的打手,草菅囚犯的人命.他们是不是象极了我们现实中的掌权阶层?我称 ...
- Codeforces 626 B. Cards (8VC Venture Cup 2016-Elimination Round)
B. Cards time limit per test 2 seconds memory limit per test 256 megabytes input standard input ...
- dart Stream
- Java 输入框复用代码
1 int messageType=JOptionPane.INFORMATION_MESSAGE; String message=mines + " minutes is approxim ...
- 输入输出流和String的混合使用-替换信息
---恢复内容开始--- package demo; import java.io.BufferedReader; import java.io.BufferedWriter; import java ...