ASP.NET MVC编程——单元测试
1自动化测试基本概念
自动化测试分为:单元测试,集成测试,验收测试。
单元测试
检验被测单元的功能,被测单元一般为低级别的组件,如一个类或类方法。
单元测试要满足四个条件:自治的,可重复的,独立的,快速的。
自治的是指:关注于验证某个单一功能,例如只关注于类的某个方法的功能。
可重复的是指:无论何时允许同一段测试代码都应该得到相同的结果。
独立的是指:不依赖与其他任何系统或单元测试。
快速的是指:所有测试都应快速地完成,
集成测试
验证两个或多个组件之间的交互。
验收测试
确保已构建的系统实现了既定的全部功能。
2准备进行单元测试
创建单元测试项目并执行测试应该依据一定的准则,运用一些技巧或工具,下面列举了常用的技巧和工具。
命名规则
测试类应以被测试的单元命名,测试方法的名称应能够描述待验证的行为。
使用特性
TestClassAttribute:标识包含测试方法的类。
TestMethodAttribute:用于标识测试方法。
TestInitializeAttribute:标识在测试之前要运行的方法,从而分配并配置测试类中的所有测试所需的资源。
ExpectedExceptionAttribute:表示测试方法的执行过程中应引发异常,用来判断抛出的异常是否符合预期。
Arrange-Act-Assert模式
此模式又被称为3A模式,Arrange,准备测试环境;Act,调用被测方法;Assert,断言。
例1:标准的3A模式,且只测试一个功能,即返回视图对象是否为null,虽然待验证的点有好几个,但我们一次只验证一个。
[TestClass]
public class HomeTest
{
[TestMethod]
public void TestCacheExeActionResultNull()
{
//Arrange
HomeController hc = new HomeController(); //Act
ViewResult vr = hc.CacheExe(); //Assert
Assert.IsNotNull(vr);
}
}
例2:验证参数为null时,是否会抛出预期的异常类型,即ArgumentNullException类型
[TestClass]
public class AccountTest
{
[TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void TestLogin()
{
AccountController ac = new AccountController(); ac.Login(null);
}
}
模拟依赖
为达到测试目的,使用假的组件模拟真实组件。有两种方式模拟依赖:一种是创建模拟对象,另一种是使用框架。为能够模拟依赖,使用存储库模式。
例1:自定义模拟对象。
控制器:
public class BookController : Controller
{
private IRepository repository;
public BookController()
: base()
{ }
public BookController(IRepository repository)
{
this.repository = repository;
}
// GET: Book
public ViewResult GetBook(int id)
{
var book = repository.GetBook(id);
return View(book);
} //其他代码
}
实现Repository
public class BookRepository:IRepository
{ public Book GetBook(int id)
{
throw new NotImplementedException();
} //其他代码
}
定义IRepository接口
public interface IRepository
{
Book GetBook(int id); //其他代码
}
实体
public class Book
{
public int Id { set; get; }
}
模拟对象
public class MocBookRepository : IRepository
{
private Book bk; public MocBookRepository(Book bk)
{
this.bk = bk;
}
public Book GetBook(int id)
{
return bk;
}
}
测试类
[TestClass]
public class BookTest
{
[TestMethod]
public void TestGetBook()
{
Book exceptedBk = new Book
{
Id =
};
BookController bc = new BookController(new MocBookRepository(exceptedBk)); ViewResult result = bc.GetBook(exceptedBk.Id); Assert.AreEqual(exceptedBk,result.Model);
}
}
例2:使用模拟框架Moq
使用nuget下载Moq,截图如下:
使用Moq:
[TestMethod]
public void TestGetBook()
{
Book exceptedBk = new Book
{
Id =
}; var mokRepository = new Moq.Mock<IRepository>();
mokRepository.Setup(rep => rep.GetBook(exceptedBk.Id)).Returns(exceptedBk); BookController bc = new BookController(mokRepository.Object);
var result = bc.GetBook(exceptedBk.Id); Assert.AreEqual(exceptedBk, result.Model);
}
重构:去除重复代码
例:
[TestClass]
public class HomeTest
{
[TestMethod]
public void TestCacheExeActionResultNull()
{
//Arrange
HomeController hc = new HomeController(); //Act
ViewResult vr = hc.CacheExe(); //Assert
Assert.IsNotNull(vr);
} [TestMethod]
public void TestCacheExeActionValue()
{
//Arrange
HomeController hc = new HomeController(); //Act
ViewResult vr = hc.CacheExe(); //Assert
Assert.AreEqual("缓存部分",vr.ViewBag.Sign);
}
}
上面面的两个测试方法含有共同的代码,应将其提取,并作为测试所需的资源,先于测试方法执行。下面是改进后的代码。
[TestClass]
public class HomeTest
{
private HomeController hc;
private ViewResult vr; [TestInitialize]
public void InitializeContext()
{
//Arrange
hc = new HomeController(); //Act
vr = hc.CacheExe();
}
[TestMethod]
public void TestCacheExeActionResultNull()
{
//Assert
Assert.IsNotNull(vr);
} [TestMethod]
public void TestCacheExeActionValue()
{
//Assert
Assert.AreEqual("缓存部分",vr.ViewBag.Sign);
}
}
3 测试ASP.NET MVC项目
3.1模拟HttpContext对象
public void HttpContextForController(Controller controller)
{
var contextBaseMock = new Mock<HttpContextBase>();
contextBaseMock.Setup(c=>c).Returns(new CustomHttpContext());
controller.ControllerContext = new ControllerContext(new RequestContext(contextBaseMock.Object, new RouteData()), controller);
} public class CustomHttpContext : HttpContextBase
{ }
3.2模拟Request对象
var contextBaseMock = new Mock<HttpContextBase>();
var method = "get";
contextBaseMock.Setup(c => c.Request.HttpMethod).Returns(method);
var mockHttpContext = contextBaseMock.Object;
或
var request = new Mock<HttpRequestBase>();
var headerValue = new NameValueCollection(){};//替换为具体实现
request.Setup(c =>c.Headers).Returns(headerValue);
var mockRequest = request.Object;
3.3模拟HttpResponse对象
var contextBaseMock = new Mock<HttpContextBase>();
contextBaseMock.Setup(c => c.Response.StatusCode).Returns();
var mockHttpContext = contextBaseMock.Object;
或
var response = new Mock<HttpResponseBase>();
var headerValue = new NameValueCollection(){};//替换为具体实现
response.Setup(c => c.Headers).Returns(headerValue);
var mockRequest = response.Object;
3.4模拟缓存对象
模拟Session对象
var contextBaseMock = new Mock<HttpContextBase>();
contextBaseMock.Setup(c => c.Session.Timeout).Returns();
var mockHttpContext = contextBaseMock.Object;
模拟Cache对象
var contextBaseMock = new Mock<HttpContextBase>();
contextBaseMock.Setup(c => c.Session.Timeout).Returns();
var mockHttpContext = contextBaseMock.Object;
3.5测试控制器
基本代码如下,其中断言部分会根据下面的测试项不同而不同
public void TestGetBook()
{
Book exceptedBk = new Book
{
Id =
}; var mokRepository = new Moq.Mock<IRepository>();
mokRepository.Setup(rep => rep.GetBook(exceptedBk.Id)).Returns(exceptedBk); BookController bc = new BookController(mokRepository.Object);
var result = bc.GetBook(exceptedBk.Id);
//断言部分 }
测试控制器操作的返回类型
Assert.IsInstanceOfType(result, typeof(ViewResult));
测试返回的视图模型数据
Assert.AreEqual(exceptedBk, result.Model);
//或
Assert.AreEqual(exceptedBk.Id,result.Model.Id);
测试重定向
控制器操作:
public RedirectResult Turn()
{
return Redirect("~/home/index");
}
测试方法:
[TestMethod]
public void TestTurn()
{
BookController bc = new BookController(); var result = bc.Turn(); Assert.AreEqual("~/home/index", result.Url);
}
3.6测试过滤器
虽然可能对控制器应用了过滤器,但单元测试调用控制器时是不会调用过滤器的;此外我们注册的全局过滤器也不会被调用。要测试过滤器,就要模拟HTTP上下文、请求等。此外,建议将具体的验证逻辑代码封装起来,这样可以将其作为普通的类来测试。
例:
动作过滤器定义:
public class CustomActionFilterAttribute : ActionFilterAttribute
{
public override void OnActionExecuted(ActionExecutedContext filterContext)
{
//具体实现
} public override void OnActionExecuting(ActionExecutingContext filterContext)
{
//具体实现
}
}
权限过滤器定义:
public class CustomAuthorizeAttribute : AuthorizeAttribute
{
private UserRole role;
public CustomAuthorizeAttribute(UserRole role)
{
this.role = role;
}
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
//具体实现
}
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
//具体实现
}
public override void OnAuthorization(AuthorizationContext filterContext)
{
base.OnAuthorization(filterContext);
}
} public enum UserRole
{
Org = ,
Vip = ,
Guest =
}
验证动作过滤器CustomActionFilterAttribute
//模拟Request
var request = new Mock<HttpRequestBase>();
request.SetupGet(r => r.HttpMethod).Returns("GET");
request.SetupGet(r => r.Url).Returns(new Uri("http://basesit/controller/action")); //设置HttpContext,用模拟的Request设置HttpContext
var httpContext = new Mock<HttpContextBase>();
httpContext.SetupGet(c => c.Request).Returns(request.Object); //模拟ActionExecutedContext
var actionExecutedContext = new Mock<ActionExecutedContext>();
actionExecutedContext.SetupGet(c => c.HttpContext).Returns(httpContext.Object); //实例化待测试过滤器CustomActionFilterAttribute
var customActionFilter = new CustomActionFilterAttribute();
//调用执行方法,执行测试
customActionFilter.OnActionExecuted(actionExecutedContext.Object); //模拟ActionExecutingContext
var actionExecutingContext = new Mock<ActionExecutingContext>();
actionExecutingContext.SetupGet(c => c.HttpContext).Returns(httpContext.Object); //调用执行方法,执行测试
customActionFilter.OnActionExecuting(actionExecutingContext.Object);
验证权限过滤器CustomAuthorizeAttribute
//模拟Request
var request = new Mock<HttpRequestBase>();
request.SetupGet(r => r.HttpMethod).Returns("GET");
request.SetupGet(r => r.Url).Returns(new Uri("http://basesit/controller/action")); //设置HttpContext,用模拟的Request设置HttpContext
var httpContext = new Mock<HttpContextBase>();
httpContext.SetupGet(c => c.Request).Returns(request.Object);
//模拟AuthorizationContext
var authorizationContext = new Mock<AuthorizationContext>();
authorizationContext.SetupGet(c => c.HttpContext).Returns(httpContext.Object); //实例化待测试权限过滤器:CustomAuthorizeAttribute
var authorizationFilter = new CustomAuthorizeAttribute(UserRole.Guest);
//调用待测试方法
authorizationFilter.OnAuthorization(authorizationContext.Object);
3.7测试视图
视图的测试主要通过实际运行,然后观察浏览器渲染出来的结果,由于浏览器种类繁多,适配是也随之变成了比较繁重的任务,依靠自动化测试不是最佳选择,至少目前不是最佳选择,但在此还是给出一个自动化测试的例子,这里使用WatiN测试套件,使用NuGet下载测试套件:
测试代码
[TestMethod]
public void TestGetBookView()
{
string url = "http://localhost/MVCPointApp/Book/GetBook/1";
using (var browser = new FireFox(url))
{
var bookDiv = browser.Div(Find.ByClass("pro_book"));
var title = bookDiv.Element(Find.First()).Text; Assert.IsFalse(string.IsNullOrWhiteSpace(title));
Assert.AreEqual("机器学习算法原理与编程实践", title);
}
}
3.8测试路由
配置的路由模板为:
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
测试被忽略的路由
[TestMethod]
public void TestIgnoreRoute()
{
var mock = new Mock<HttpContextBase>();
mock.Setup(m => m.Request.AppRelativeCurrentExecutionFilePath).Returns("~/book.axd");
var routes = new RouteCollection(); var routeData = routes.GetRouteData(mock.Object); Assert.IsNull(routeData);
Assert.IsInstanceOfType(routeData.RouteHandler,typeof(StopRoutingHandler));
}
测试可匹配的路由
[TestMethod]
public void TestMatchedRoute()
{
var mock = new Mock<HttpContextBase>();
mock.Setup(m => m.Request.AppRelativeCurrentExecutionFilePath).Returns("~/book/getbook/1");
var routes = new RouteCollection(); var routeData = routes.GetRouteData(mock.Object); Assert.IsNull(routeData);
Assert.AreEqual("Book", routeData.Values["controller"]);
Assert.AreEqual("GetBook", routeData.Values["action"]);
Assert.AreEqual(UrlParameter.Optional, routeData.Values["id"]); }
4启发:开发可测试的程序
即使对下面的概念没有感觉,当实施一次单元测试以后就会深有体会。
基于接口编程
基于接口的编程,使得可以在测试的时候指定具体的类型,这样解除了依赖,方便模拟组件。我们常见的相关概念是控制反转(依赖注入)
使用IoC框架
使用成熟稳定的Ioc框架减少待测试的代码量,减轻测试任务量。
存储库模式
使用存储库模式,将数据访问逻辑与业务逻辑、控制器分离开来,测试控制器时可以借助此模式方便地模拟依赖,这样将模块合理地切分,实现测试只关注单一功能。
面向切面编程(APO)
面向切面编程是面向对象编程的有力补充,降低业务处理中各个部分之间的耦合性,便于实施单元测试。
测试驱动开发(TDD)
遵循“红灯-绿灯-重构”的原则:从失败的情况开始测试,然后编写最少的代码让测试通过。为了能尽快地通过测试,编写的最少量的代码可能是未经过深思熟虑的,这种情况下就要重构。
参考:
1.Jess Chadwick/Todd Snyder/Hrusikesh Panda,徐雷/徐扬译。ASP.NET MVC4 Web编程
2.Jon Galloway/Phil Haack/Brad Wilson/K. Scott Allen,孙远帅/邹权译 ASP.NET MVC4 高级编程(第四版)
3.Dino Esposito著,潘丽臣译,ASP.NET MVC5编程实战
转载与引用请注明出处。 时间仓促,水平有限,如有不当之处,欢迎指正。
ASP.NET MVC编程——单元测试的更多相关文章
- ASP.NET MVC 编程参考
ASP.NET MVC 编程参考 转载请注明出处:http://surfsky.cnblogs.com MVC 参考 http://msdn.microsoft.com/zh-cn/dd40 ...
- 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编程——视图
1Razon语法 使用@符号后接C#或VB.NET语句的方式. 基本规则 1)变量 @后直接变量即可 2)代码块 为使用表达式或多行代码,@后跟大括号将多行代码包括在大括号中 3)"+&qu ...
- ASP.NET MVC编程——控制器
每一个请求都会经过控制器处理,控制器中的每个方法被称为控制器操作,它处理具体的请求. 1操作输入参数 控制器的操作的输入参数可以是内置类型也可以是自定义类型. 2操作返回结果 结果类型 调用方法 备注 ...
- ASP.NET MVC编程——验证、授权与安全
1 验证 一般采用表单验证完成登陆验证,建议结合SSL使用.为限制控制器只能执行HTTPS,使用RequireHttpsAttribute 2 授权 对账户的权限的控制可以通过在控制器或控制器操作上 ...
- ASP.NET MVC编程——模型
1 ViewModel 是一种专门提供给View使用的模型,使用ViewModel的理由是实体或领域模型所包含的属性比View使用的多或少,这种情况下实体或领域模型不适合View使用. 2模型绑定 默 ...
- ASP.NET MVC编程——错误处理与日记
ASP.NET MVC的错误处理应考虑到这几个方面:模型绑定期间发生的错误,未能路由到指定操作,针对控制器的错误处理.使用配置文件可以帮助我们处理异常,但是不够灵活和全面:使用HandleErrorA ...
- ASP.NET MVC编程——路由
框架自动生成的路由配置 上图中,路由配置文件为App_Start文件夹下的RouteConfig.cs. 代码如下: public class RouteConfig { public static ...
随机推荐
- 【Luogu3804】【模板】后缀自动机(后缀自动机)
[Luogu3804][模板]后缀自动机(后缀自动机) 题面 洛谷 题解 一个串的出现次数等于\(right/endpos\)集合的大小 而这个集合的大小等于所有\(parent\)树上儿子的大小 这 ...
- 【Luogu2759】奇怪的函数(数论)
[Luogu2759]奇怪的函数(数论) 题面 题目描述 使得 \(x^{x}\)达到或超过 n 位数字的最小正整数 x 是多少? 输入输出格式 输入格式: 一个正整数 n 输出格式: 使得 \(x^ ...
- 【UVA 11426】gcd之和 (改编)
题面 \(\sum_{i=1}^{n}\sum_{j=1}^m\gcd(i,j)\mod998244353\) \(n,m<=10^7\) Sol 简单的一道莫比乌斯反演题 \(原式=\sum_ ...
- mysql大小写敏感问题
问题: 在创建mysql表的时候发现不论表明是大写或小写,建完之后统一被变成了小写. 原因: MySQL在windows下是不区分大小写的,将script文件导入MySQL后表名也会自动转化为小写. ...
- lambda表达式封装对数据库的查询
前言: 1.为什么要封装lambda表达式数据库查询,原因有一下几点: 1.1.在以往的开发中进行数据库表查询时,其实所需要的字段就是其中几个,但是在开发中,开发者往往习惯select * 进行查询, ...
- AJAX跨域问题解决方法(3)——被调用方解决跨域
被调用方解决跨域是指在HTTP响应头中增加指定的字段,允许调用方调用 可以在两种地方增加1.apache/nginx(HTTP服务器)2.tomcat(应用服务器) 浏览器如何判断跨域?仔细观察可以发 ...
- git仓库搭建及客户端使用
这里只在linux上做git仓库搭建 这里只在linux上做git仓库搭建 这里只在linux上做git仓库搭建 linux 服务器上安装及配置git 一.安装git yum install -y g ...
- Python函数学习——递归
递归函数 在函数内部,可以调用其他函数.如果一个函数在内部调用自身本身,这个函数就是递归函数. 函数实现过程 def calc(n): v = int(n//2) print(v) if v > ...
- 设计模式——桥接模式(C++实现)
[root@ ~/learn_code/design_pattern/18_bridge]$ cat Abstraction.h #ifndef _ABSTRACTION_H_ #define _AB ...
- input依次输入密码
原理: 一个真正的可以输入的input框,opacity: 0,设定位层级:(视图不可见的) 再来6(n)个input,readyonly,用来显示,type为password,设置好样式:(视图可见 ...