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

一、模型状态-ModelState

我理解的ModelState是微软在ASP.NETMVC中提出的一种新机制,它主要实现以下几个功能:

1.保存客户端传过来的数据,如果验证不通过,把数据返回到客户端,这样可以保存用户输入,不需要重新输入。

2.验证数据,以及保存数据对应的错误信息。

3.微软的一种DRY(Don'tRepeatYourself)设计,通过ModelState可以做服务端验证,同时可以配合jqueryvalidation生成前端数据验证。

但是在WebAPI里面,ModelState的主要功能就只剩下第2点了。

需要注意的是,ModelState一般只做输入验证,一些其他的业务验证还有要在特定的地方进行处理。

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

二、数据注解-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测试效果如下:

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

四、全局数据验证

我们在使用数据验证的时候,往往会出现许多重复的代码,如下图:

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

有没有办法减少这些重复的代码呢?我从“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描述:

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

测试代码:

[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描述:

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

测试代码:

[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描述:

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

测试代码:

[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);

}

}

单元测试结果:

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

说明:

GlobalActionFilterAttributeTests是单元测试的父类,公共的部分可以抽取到这里。其中ContextUtil是微软源码中的测试辅助类。

publicclassGlobalActionFilterAttributeTests

{

protectedreadonlyMock<HttpActionDescriptor>ActionDescriptorMock=newMock<HttpActionDescriptor>();

protectedreadonlyMock<HttpControllerDescriptor>ControllerDescriptorMock=newMock<HttpControllerDescriptor>();

protectedHttpActionContextHttpActionContext;

protectedGlobalActionFilterAttributeGlobalActionFilterAttribute;

publicGlobalActionFilterAttributeTests()

{

HttpActionContext=ContextUtil.CreateActionContext();

GlobalActionFilterAttribute=newGlobalActionFilterAttribute();

}

}

相关推荐