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 = 1 }; 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 = 1 }; 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(200);
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(10); var mockHttpContext = contextBaseMock.Object; |
模拟Cache对象
var contextBaseMock = new Mock<HttpContextBase>();
contextBaseMock.Setup(c => c.Session.Timeout).Returns(10);
var mockHttpContext = contextBaseMock.Object;
3.5测试控制器
基本代码如下,其中断言部分会根据下面的测试项不同而不同
public void TestGetBook() { Book exceptedBk = new Book { Id = 1 }; 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 = 1, Vip = 2, Guest = 3 } |
验证动作过滤器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)
遵循“红灯-绿灯-重构”的原则:从失败的情况开始测试,然后编写最少的代码让测试通过。为了能尽快地通过测试,编写的最少量的代码可能是未经过深思熟虑的,这种情况下就要重构。
上文内容不用于商业目的,如涉及知识产权问题,请权利人联系博为峰小编(021-64471599-8017),我们将立即处理。