软件测试Web API之数据验证与单元测试
一、模型状态-ModelState
我理解的ModelState是微软在ASP.NETMVC中提出的一种新机制,它主要实现以下几个功能:
1.保存客户端传过来的数据,如果验证不通过,把数据返回到客户端,这样可以保存用户输入,不需要重新输入。
2.验证数据,以及保存数据对应的错误信息。
3.微软的一种DRY(Don'tRepeatYourself)设计,通过ModelState可以做服务端验证,同时可以配合jqueryvalidation生成前端数据验证。
但是在WebAPI里面,ModelState的主要功能就只剩下第2点了。
需要注意的是,ModelState一般只做输入验证,一些其他的业务验证还有要在特定的地方进行处理。
二、数据注解-DataAnnotations
数据注解可以理解为验证数据的逻辑或方法,微软本身有提供一批数据注解,当然我们也可以自定义数据注解,以下是微软提供的常见的数据注解:
1.Required-非空验证。
当一个输入是null时会引发一个验证错误。
当属性类型是string的时候,如果设置了AllowEmptyStrings=false(默认为false),那么输入空字符串或者空格,也会引发一个验证错误。
[Required]
publicstringName{get;set;}
[Required(AllowEmptyStrings=true)]
publicstringExchange{get;set;}
2.StringLength-长度验证。
当输入大于指定最大长度,或者小于最大指定长度时,会引发一个验证错误。
[StringLength(100)]
publicstringSymbol{get;set;}
[StringLength(100,MinimumLength=10)]
publicstringName{get;set;}
3.RegularExpression-正则表达式验证。
当输入内容不满足指定的正则表达式时,会引发一个验证错误。
注:在.NETFramework4.6.1添加了一个MatchTimeoutInMilliseconds属性,用来设定正则表达时验证时长。如超时,则抛出RegexMatchTimeoutException异常。
[RegularExpression("yourexpression")]
publicstringSymbol{get;set;}
4.Range-值范围验证
当输入的值小于最小值或者大于最大值时,会引发一个验证错误,这里要求验证字段的类型需要实现IComparable接口。
[Range(10,100)]
publicdoubleOpenPrice{get;set;}
[Range(typeof(double),"10","100")]
publicdoubleClosePrice{get;set;}
5.Compare-对比验证
确保对象两个属性拥有相同的值。如果两个值不同,会引发一个验证错误。
publicstringName{get;set;}
[Compare("Name")]
publicstringConfirmName{get;set;}
6.Remote-远程调用验证
Remote可以利用服务端回调函数执行客户端的验证逻辑。
注:该数据注解是ASP.NETMVC特有的注解,在WebApi中无此注解。
[Remote("CheckName","Account"]
publicstringUserName{get;set;}
publicclassAccountController:Controller
{
publicJsonResultCheckName(stringname)
{
returnJson(true);
}
}
三、自定义数据注解
如果觉得微软提供的数据注解不够用,也可以自己写数据注解,只需要继承ValidationAttribute,并复写IsValid方法。
下面是一个来自《ASP.NETMVC5高级编程》的一个例子MaxWordsAttribute,用于限制属性的单词个数。
publicclassMaxWordsAttribute:ValidationAttribute
{
privatereadonlyint_maxWords;
publicMaxWordsAttribute(intmaxWords)
{
_maxWords=maxWords;
}
protectedoverrideValidationResultIsValid(objectvalue,ValidationContextvalidationContext)
{
if(value!=null)
{
varvalueAsString=value.ToString();
if(valueAsString.Split('').Length>_maxWords)
{
returnnewValidationResult("Toomanywords!");
}
}
returnValidationResult.Success;
}
}
[Required]
[MaxWords(2)]
publicstringName{get;set;}
[HttpPost]
publicIHttpActionResultCreate(Stockstock)
{
if(!ModelState.IsValid)
{
returnBadRequest(ModelState);
}
returnCreatedAtRoute("Get",new{symbol=stock.Symbol},stock);
}
SwashbuckleHelpPage测试效果如下:
四、全局数据验证
我们在使用数据验证的时候,往往会出现许多重复的代码,如下图:
有没有办法减少这些重复的代码呢?我从“ModelValidationinASP.NETWebAPI”这篇文章中找到了方法。
首先,我们需要写一个GlobalActionFilterAttribute。
publicclassGlobalActionFilterAttribute:ActionFilterAttribute
{
publicoverridevoidOnActionExecuting(HttpActionContextactionContext)
{
if(actionContext.ModelState.IsValid==false)
{
actionContext.Response=actionContext.Request.CreateErrorResponse(HttpStatusCode.BadRequest,actionContext.ModelState);
}
}
}
然后,在WebApiConfig里注册一下这个Attribute。
publicstaticvoidRegister(HttpConfigurationconfig)
{
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute("DefaultApi","api/{controller}/{id}",new{id=RouteParameter.Optional});
//registerthecustomactionfilter
config.Filters.Add(newGlobalActionFilterAttribute());
}
那么,我们把Controller中的数据验证注释掉,依旧会得到相同的效果。
如果想只对Post请求进行验证,可以在GlobalActionFilterAttribute加对请求方式的判断:
publicclassGlobalActionFilterAttribute:ActionFilterAttribute
{
publicoverridevoidOnActionExecuting(HttpActionContextactionContext)
{
//Ifyouonlywanttovalidatethepostrequest.
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)]
publicsealedclassBypassModelStateValidationAttribute:Attribute
{
}
2.在不需要验证的Controller或者Action上加这个Attribute
[HttpPut]
[BypassModelStateValidation]
publicIHttpActionResultUpdate(Stockstock)
{
//if(!ModelState.IsValid)
//{
//returnBadRequest(ModelState);
//}
returnStatusCode(HttpStatusCode.NoContent);
}
3.在GlobalActionFilterAttribute加对BypassModelStateValidationAttribute的判断:
publicclassGlobalActionFilterAttribute:ActionFilterAttribute
{
publicoverridevoidOnActionExecuting(HttpActionContextactionContext)
{
//Ifyouonlywanttovalidatethepostrequest.
if(actionContext.Request.Method!=HttpMethod.Post)
{
return;
}
varpassby=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")]
publicclassHttpMethodNotMatchedTest:GlobalActionFilterAttributeTests
{
[Given(@"非Post方式的请求")]
publicvoidGiven()
{
HttpActionContext.Request.Method=HttpMethod.Get;
}
[When(@"执行OnActionExecuting方法")]
publicvoidWhen()
{
GlobalActionFilterAttribute.OnActionExecuting(HttpActionContext);
}
[Then(@"Response为空")]
publicvoidThen()
{
Assert.IsNull(HttpActionContext.Response);
}
}
2.设置了跳过验证-BypassModelStateValidation
feature描述:
测试代码:
[Binding]
[Scope(Scenario=@"BypassModelStateValidation")]
publicclassBypassModelStateValidationTest:GlobalActionFilterAttributeTests
{
[Given(@"BypassModelStateValidationAttribute")]
publicvoidGiven()
{
HttpActionContext.Request.Method=HttpMethod.Post;
HttpActionContext.ActionDescriptor=ActionDescriptorMock.Object;
ActionDescriptorMock.Setup(m=>m.GetCustomAttributes<BypassModelStateValidationAttribute>()).Returns(newCollection<BypassModelStateValidationAttribute>(new[]{newBypassModelStateValidationAttribute()}));
HttpActionContext.ControllerContext.ControllerDescriptor=ControllerDescriptorMock.Object;
ControllerDescriptorMock.Setup(m=>m.GetCustomAttributes<BypassModelStateValidationAttribute>()).Returns(newCollection<BypassModelStateValidationAttribute>());
}
[When(@"执行OnActionExecuting方法")]
publicvoidWhen()
{
GlobalActionFilterAttribute.OnActionExecuting(HttpActionContext);
}
[Then(@"Response为空")]
publicvoidThen()
{
Assert.IsNull(HttpActionContext.Response);
}
}
3.验证不通过-ModelStateInvalid
feature描述:
测试代码:
[Binding]
[Scope(Scenario=@"ModelStateInvalid")]
publicclassModelStateInvalidTest:GlobalActionFilterAttributeTests
{
[Given(@"ModelState错误信息")]
publicvoidGiven()
{
HttpActionContext.Request.Method=HttpMethod.Post;
HttpActionContext.ActionDescriptor=ActionDescriptorMock.Object;
ActionDescriptorMock.Setup(m=>m.GetCustomAttributes<BypassModelStateValidationAttribute>()).Returns(newCollection<BypassModelStateValidationAttribute>());
HttpActionContext.ControllerContext.ControllerDescriptor=ControllerDescriptorMock.Object;
ControllerDescriptorMock.Setup(m=>m.GetCustomAttributes<BypassModelStateValidationAttribute>()).Returns(newCollection<BypassModelStateValidationAttribute>());
HttpActionContext.ModelState.AddModelError("stock.Name","TheNamefieldisrequired.");
}
[When(@"执行OnActionExecuting方法")]
publicvoidWhen()
{
GlobalActionFilterAttribute.OnActionExecuting(HttpActionContext);
}
[Then(@"返回BadRequest")]
publicvoidThen()
{
Assert.AreEqual(HttpStatusCode.BadRequest,HttpActionContext.Response.StatusCode);
}
}
单元测试结果:
说明:
GlobalActionFilterAttributeTests是单元测试的父类,公共的部分可以抽取到这里。其中ContextUtil是微软源码中的测试辅助类。
publicclassGlobalActionFilterAttributeTests
{
protectedreadonlyMock<HttpActionDescriptor>ActionDescriptorMock=newMock<HttpActionDescriptor>();
protectedreadonlyMock<HttpControllerDescriptor>ControllerDescriptorMock=newMock<HttpControllerDescriptor>();
protectedHttpActionContextHttpActionContext;
protectedGlobalActionFilterAttributeGlobalActionFilterAttribute;
publicGlobalActionFilterAttributeTests()
{
HttpActionContext=ContextUtil.CreateActionContext();
GlobalActionFilterAttribute=newGlobalActionFilterAttribute();
}
}