Web API 之数据验证与单元测试

发表于:2018-3-12 11:32

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

 作者:DotNet    来源:编程玩家

  一、模型状态 - ModelState
  我理解的ModelState是微软在ASP.NET MVC中提出的一种新机制,它主要实现以下几个功能:
  1. 保存客户端传过来的数据,如果验证不通过,把数据返回到客户端,这样可以保存用户输入,不需要重新输入。
  2. 验证数据,以及保存数据对应的错误信息。
  3. 微软的一种DRY(Don't Repeat Yourself)设计,通过ModelState可以做服务端验证,同时可以配合jquery validation生成前端数据验证。
  但是在Web API里面,ModelState的主要功能就只剩下第2点了。
  需要注意的是,ModelState一般只做输入验证,一些其他的业务验证还有要在特定的地方进行处理。
  二、数据注解 - Data Annotations
  数据注解可以理解为验证数据的逻辑或方法,微软本身有提供一批数据注解,当然我们也可以自定义数据注解,以下是微软提供的常见的数据注解:
  1. Required - 非空验证。
  当一个输入是null时会引发一个验证错误。
  当属性类型是string的时候,如果设置了AllowEmptyStrings = false(默认为false),那么输入空字符串或者空格,也会引发一个验证错误。
  [Required]
  public string Name { get; set; }
  [Required(AllowEmptyStrings = true)]
  public string Exchange { get; set; }
  2. StringLength - 长度验证。
  当输入大于指定最大长度,或者小于最大指定长度时,会引发一个验证错误。 
  [StringLength(100)]
  public string Symbol { get; set; }
  [StringLength(100, MinimumLength = 10)]
  public string Name { get; set; }
  3. RegularExpression - 正则表达式验证。
  当输入内容不满足指定的正则表达式时,会引发一个验证错误。
  注:在.NET Framework 4.6.1添加了一个MatchTimeoutInMilliseconds属性,用来设定正则表达时验证时长。如超时,则抛出RegexMatchTimeoutException异常。
  [RegularExpression("your expression")]
  public string Symbol { get; set; }
  4. Range - 值范围验证
  当输入的值小于最小值或者大于最大值时,会引发一个验证错误,这里要求验证字段的类型需要实现IComparable接口。
  [Range(10, 100)]
  public double OpenPrice { get; set; }
  [Range(typeof(double), "10", "100")]
  public double ClosePrice { get; set; }
  5. Compare - 对比验证
  确保对象两个属性拥有相同的值。如果两个值不同,会引发一个验证错误。
  public string Name { get; set; }
  [Compare("Name")]
  public string ConfirmName { get; set; }
  6. Remote - 远程调用验证
  Remote可以利用服务端回调函数执行客户端的验证逻辑。
  注:该数据注解是ASP.NET MVC特有的注解,在Web Api中无此注解。
  [Remote("CheckName", "Account"]
  public string UserName{ get; set; }
  public class AccountController: Controller
  {
      public JsonResult CheckName(string name)
      {
           return Json(true);       
      }
  }
  三、自定义数据注解
  如果觉得微软提供的数据注解不够用,也可以自己写数据注解,只需要继承ValidationAttribute,并复写IsValid方法。
  下面是一个来自《ASP.NET MVC 5高级编程》的一个例子MaxWordsAttribute,用于限制属性的单词个数。
  public class MaxWordsAttribute : ValidationAttribute
  {
      private readonly int _maxWords;
      public MaxWordsAttribute(int maxWords)
      {
          _maxWords = maxWords;
      }
      protected override ValidationResult IsValid(object value, ValidationContext validationContext)
      {
          if (value != null)
          {
              var valueAsString = value.ToString();
              if (valueAsString.Split(' ').Length > _maxWords)
              {
                  return new ValidationResult("Too many words!");
              }
          }
          return ValidationResult.Success;
      }
  }
  [Required]
  [MaxWords(2)]
  public string Name { get; set; }
  [HttpPost]
  public IHttpActionResult Create(Stock stock)
  {
      if (!ModelState.IsValid)
      {
          return BadRequest(ModelState); 
      }
      return CreatedAtRoute("Get", new { symbol = stock.Symbol }, stock);
  }
  Swashbuckle Help Page测试效果如下:
  
  四、全局数据验证
  我们在使用数据验证的时候,往往会出现许多重复的代码,如下图:
  有没有办法减少这些重复的代码呢?我从“Model Validation in ASP.NET Web API”这篇文章中找到了方法。
  首先,我们需要写一个GlobalActionFilterAttribute。
  public class GlobalActionFilterAttribute: ActionFilterAttribute
  {
      public override void OnActionExecuting(HttpActionContext actionContext)
      {
          if (actionContext.ModelState.IsValid == false)
          {
              actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.BadRequest, actionContext.ModelState);
          }
      }
  }
  然后,在WebApiConfig里注册一下这个Attribute。
  public static void Register(HttpConfiguration config)
  {
      config.MapHttpAttributeRoutes();
      config.Routes.MapHttpRoute("DefaultApi", "api/{controller}/{id}", new { id = RouteParameter.Optional } );
      //register the custom action filter
      config.Filters.Add(new GlobalActionFilterAttribute());
  } 
   那么,我们把Controller中的数据验证注释掉,依旧会得到相同的效果。
  如果想只对Post请求进行验证,可以在GlobalActionFilterAttribute加对请求方式的判断:
  public class GlobalActionFilterAttribute : ActionFilterAttribute
  {
      public override void OnActionExecuting(HttpActionContext actionContext)
      {
          //If you only want to validate the post request.
          if (actionContext.Request.Method != HttpMethod.Post)
          {
              return;
          }
          if (actionContext.ModelState.IsValid == false)
          {
              actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.BadRequest, actionContext.ModelState);
          }
      }
  }
  如果某些Controller或Action需要绕过数据验证,那么可以这么实现:
  1. 定义一个BypassModelStateValidationAttribute
  [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = false)]
  public sealed class BypassModelStateValidationAttribute : Attribute
  {
  }
  2. 在不需要验证的Controller或者Action上加这个Attribute
  [HttpPut]
  [BypassModelStateValidation]
  public IHttpActionResult Update(Stock stock)
  {
      //if (!ModelState.IsValid)
      //{
      //    return BadRequest(ModelState);
      //}
      return StatusCode(HttpStatusCode.NoContent);
  } 
   
  3. 在GlobalActionFilterAttribute加对BypassModelStateValidationAttribute的判断:
  public class GlobalActionFilterAttribute : ActionFilterAttribute
  {
      public override void OnActionExecuting(HttpActionContext actionContext)
      {
          //If you only want to validate the post request.
          if (actionContext.Request.Method != HttpMethod.Post)
          {
              return;
          }
          var passby = actionContext.ActionDescriptor.GetCustomAttributes<BypassModelStateValidationAttribute>().Any() ||
                       actionContext.ControllerContext.ControllerDescriptor.GetCustomAttributes<BypassModelStateValidationAttribute>().Any();
          if (passby)
          {
              return;
          }
          if (actionContext.ModelState.IsValid == false)
          {
              actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.BadRequest, actionContext.ModelState);
          }
      }
  }
  五、单元测试
  我使用BDD的风格编写单元测试,关于BDD的详细信息,可查看我之前的文章《行为驱动开发(BDD)实践示例》(http://www.cnblogs.com/Erik_Xu/p/5297981.html)。
  对于全局数据验证,我设计了3个测试用例
  1. 非Post请求不做验证 - HttpMethodNotMatched
  feature描述:
  测试代码:
  [Binding]
  [Scope(Scenario = @"HttpMethodNotMatched")]
  public class HttpMethodNotMatchedTest : GlobalActionFilterAttributeTests
  {
      [Given(@"非Post方式的请求")]
      public void Given()
      {
          HttpActionContext.Request.Method = HttpMethod.Get;
      }
      [When(@"执行OnActionExecuting方法")]
      public void When()
      {
          GlobalActionFilterAttribute.OnActionExecuting(HttpActionContext);
      }
      [Then(@"Response为空")]
      public void Then()
      {
          Assert.IsNull(HttpActionContext.Response);
      }
  }
  2. 设置了跳过验证 - BypassModelStateValidation
  feature描述:
  测试代码:
  [Binding]
  [Scope(Scenario = @"BypassModelStateValidation")]
  public class BypassModelStateValidationTest : GlobalActionFilterAttributeTests
  {
      [Given(@"BypassModelStateValidationAttribute")]
      public void Given()
      {
          HttpActionContext.Request.Method = HttpMethod.Post;
          HttpActionContext.ActionDescriptor = ActionDescriptorMock.Object;
          ActionDescriptorMock.Setup(m => m.GetCustomAttributes<BypassModelStateValidationAttribute>()).Returns(new Collection<BypassModelStateValidationAttribute>(new[] { new BypassModelStateValidationAttribute() }));
          HttpActionContext.ControllerContext.ControllerDescriptor = ControllerDescriptorMock.Object;
          ControllerDescriptorMock.Setup(m => m.GetCustomAttributes<BypassModelStateValidationAttribute>()).Returns(new Collection<BypassModelStateValidationAttribute>());  
      }
      [When(@"执行OnActionExecuting方法")]
      public void When()
      {
          GlobalActionFilterAttribute.OnActionExecuting(HttpActionContext);
      }
      [Then(@"Response为空")]
      public void Then()
      {
          Assert.IsNull(HttpActionContext.Response);
      }
  }
  3. 验证不通过 - ModelStateInvalid
  feature描述:
  测试代码:
  [Binding]
  [Scope(Scenario = @"ModelStateInvalid")]
  public class ModelStateInvalidTest : GlobalActionFilterAttributeTests
  {
      [Given(@"ModelState错误信息")]
      public void Given()
      {
          HttpActionContext.Request.Method = HttpMethod.Post;
          HttpActionContext.ActionDescriptor = ActionDescriptorMock.Object;
          ActionDescriptorMock.Setup(m => m.GetCustomAttributes<BypassModelStateValidationAttribute>()).Returns(new Collection<BypassModelStateValidationAttribute>());
          HttpActionContext.ControllerContext.ControllerDescriptor = ControllerDescriptorMock.Object;
          ControllerDescriptorMock.Setup(m => m.GetCustomAttributes<BypassModelStateValidationAttribute>()).Returns(new Collection<BypassModelStateValidationAttribute>());
          HttpActionContext.ModelState.AddModelError("stock.Name", "The Name field is required.");
      }
      [When(@"执行OnActionExecuting方法")]
      public void When()
      {
          GlobalActionFilterAttribute.OnActionExecuting(HttpActionContext);
      }
      [Then(@"返回Bad Request")]
      public void Then()
      {
          Assert.AreEqual(HttpStatusCode.BadRequest, HttpActionContext.Response.StatusCode);
      }
  }
  单元测试结果:
  说明:
  GlobalActionFilterAttributeTests是单元测试的父类,公共的部分可以抽取到这里。其中ContextUtil是微软源码中的测试辅助类。
  public class GlobalActionFilterAttributeTests
  {
      protected readonly Mock<HttpActionDescriptor> ActionDescriptorMock = new Mock<HttpActionDescriptor>();
      protected readonly Mock<HttpControllerDescriptor> ControllerDescriptorMock = new Mock<HttpControllerDescriptor>();
      protected HttpActionContext HttpActionContext;
      protected GlobalActionFilterAttribute GlobalActionFilterAttribute;
      public GlobalActionFilterAttributeTests()
      {
          HttpActionContext = ContextUtil.CreateActionContext();
          GlobalActionFilterAttribute = new GlobalActionFilterAttribute();
      }
  }
  源码下载 :https://github.com/ErikXu/WebApi.Trial



上文内容不用于商业目的,如涉及知识产权问题,请权利人联系博为峰小编(021-64471599-8017),我们将立即处理。
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

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

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

沪ICP备05003035号

沪公网安备 31010102002173号