【问题标题】:Unit Testing a Custom Validation Filter对自定义验证过滤器进行单元测试
【发布时间】:2015-01-23 07:47:26
【问题描述】:

我有以下属性:

public class ValidateModelAttribute : ActionFilterAttribute
    {
        public override void OnActionExecuting(HttpActionContext actionContext)
        {
            if (actionContext.ModelState.IsValid == false)
            {
                actionContext.Response = actionContext.Request.CreateErrorResponse(
                    HttpStatusCode.BadRequest, actionContext.ModelState);
            }
        }
    }  

我有这个通用扩展方法来确定属性是否应用于方法

public static bool ActionHasFilter(this ApiController controller, string action, Type filter)
    {
        var controllerType = controller.GetType();
        var method = controllerType.GetMethod(action);

        object[] filters = method.GetCustomAttributes(filter, true);

        return filters.Any(x=>x.GetType() == filter);

    }

我的问题是如何在不测试控制器操作的情况下测试属性是否真正起作用?

假设我有以下实体

public class UserViewModel
{
     [Required]
     public string Name {get; set;}
     [Required]
     [EmailAddress]
     public string Email {get;set;
}

我将如何模拟上下文并检查模型是否有效?

我正在使用 Nunit 和 Moq。

【问题讨论】:

    标签: unit-testing asp.net-web-api nunit moq


    【解决方案1】:

    您可以按如下方式执行此操作。 您需要考虑的几件事。

    1. ModelState 不需要被剔除,您只需添加导致 actionContext.ModelState.IsValid 为 false 的模型错误。

      contextStub.Object.ModelState.AddModelError("key", "error");

      你也不需要 UserViewModel

    2. 您不能使用 Moq 来设置 actionContext.Request,因为 Request 属性是非虚拟的。它也有一个 getter,但你也不能提供自己的 HttpRequestMessage。

    要解决这个问题,您需要抽象 HttpActionContext 并创建 Virtual 属性,以便 Moq 可以提供设置。一种技术可以称为Extract Override

    public class TestableHttpActionContext : HttpActionContext
    {
        public virtual new HttpRequestMessage  Request  { get; set; }
    }
    

    现在你的动作方法可以修改如下。

    public class ValidateModelAttribute : ActionFilterAttribute
    {
        public override void OnActionExecuting(HttpActionContext actionContext)
        {            
            TestableActionExecuting((TestableHttpActionContext)actionContext);
        }
    
        private void TestableActionExecuting(TestableHttpActionContext actionContext)
        {
            if (actionContext.ModelState.IsValid == false)
            {
                actionContext.Response = actionContext.Request.CreateErrorResponse(
                    HttpStatusCode.BadRequest, actionContext.ModelState);
            }
        }
    }
    

    现在您可以对 OnActionExecuting 进行单元测试,如下所示。

    [TestFixture]
    public class UnitTest1
    {
        [Test]
        public void OnActionExecuting_ErrorResponse_ExpectBadRequest()
        {
            var sut = new ValidateModelAttribute();
            var contextStub = new Mock<TestableHttpActionContext>();
            contextStub.Object.ModelState.AddModelError("key", "error");
            contextStub.Setup(x => x.Request).Returns(new HttpRequestMessage());
    
            sut.OnActionExecuting(contextStub.Object);
    
            Assert.AreEqual("BadRequest", 
                             contextStub.Object.Response.StatusCode.ToString());
        }
    }
    

    【讨论】:

    • 您的代码将适用于单元测试,但不适用于已实现的属性,因为 HttpActionContext 无法转换为 TestableHttpActionContext。
    • 您只需要使用 HttpActionContext 而不是 TestableHttpActionContext。它只是促进可测试的行为。重要提示:TestableHttpActionContext 没有任何行为,仅用于测试。
    • 我知道,但是您的代码在生产环境中不起作用。当使用常规 HttpActionContext 触发 OnActionExecuting 时,它会抛出异常,因为它无法强制转换为 TestableHttpActionContext。
    • 对不起,我错过了。感谢您指出。我认为@Prutswonder 的答案/方向更适合我。
    【解决方案2】:

    Spock's solution 在正确的轨道上,但对代码的侵入性有点太大,因为它使ValidateModelAttribute 类依赖于TestableHttpActionContext。我的实现使用了一个公共属性,该属性将用于为单元测试“注入”Request 对象,而作为属性的实现继续使用来自ActionContextRequest 对象:

    public class ValidateModelAttribute : ActionFilterAttribute
    {
        public HttpRequestMessage TestRequestMessage { get; set; }
    
        public override void OnActionExecuting(HttpActionContext actionContext)
        {
            PerformValidation(actionContext, TestRequestMessage ?? actionContext.Request);
        }
    
        private void PerformValidation(HttpActionContext actionContext, HttpRequestMessage request)
        {
            if (actionContext.ModelState.IsValid == false)
            {
                actionContext.Response = request.CreateErrorResponse(HttpStatusCode.BadRequest, actionContext.ModelState);
            }
        }
    }
    

    单元测试:

    [Test]
    public void OnActionExecuting_ValidModel_ResponseIsNotSet()
    {
        var actionContext = new HttpActionContext();
    
        actionContext.ModelState.Clear();
    
        var attribute = new ValidateModelAttribute { TestRequestMessage = new HttpRequestMessage() };
    
        attribute.OnActionExecuting(actionContext);
    
        Assert.IsNull(actionContext.Response);
    }
    
    [Test]
    public void OnActionExecuting_InvalidModel_ResponseIsSetToBadRequest()
    {
        var actionContext = new HttpActionContext();
    
        actionContext.ModelState.AddModelError("key", "error");
    
        var attribute = new ValidateModelAttribute() { TestRequestMessage = new HttpRequestMessage() };
    
        attribute.OnActionExecuting(actionContext);
    
        Assert.AreEqual(HttpStatusCode.BadRequest, actionContext.Response.StatusCode);
    }
    

    请注意,我没有使用实际模型来验证ModelState,因为这超出了单元测试的范围:我们要测试ModelState 的结果,而不是ModelState 本身。 ;-)

    【讨论】:

    • 太好了,我也认为这是正确的方法。当某些 WEB API 类型本身不可测试时,太多的混乱是可耻的。
    • 如果正在测试的过滤器使用request.CreateResponse(),请务必使用包含configuration 参数的重载,并提供与上述类似解析的HttpConfiguration TestConfiguration
    猜你喜欢
    • 2017-02-18
    • 1970-01-01
    • 1970-01-01
    • 2021-06-24
    • 1970-01-01
    • 1970-01-01
    • 2017-02-16
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多