ASP.NET MVC编程——单元测试

发表于:2018-4-03 16:14

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:甜橙很酸    来源:51testing软件测试网采编

  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),我们将立即处理。
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号