使用Service Locator时如何为ActionFilter编写单元测试

时间:2016-07-09 19:55:23

标签: unit-testing asp.net-core asp.net-core-mvc asp.net-core-1.0

我打算写一个ActionFilter用于业务验证,其中一些服务将通过Service Locator解决(我知道这不是一个好的做法,我尽可能避免服务定位器模式,但为此我想用它)。

过滤器的

OnActionExecuting方法是这样的:

    public override void OnActionExecuting(ActionExecutingContext actionContext)
    {
        // get validator for input;
        var validator = actionContext.HttpContext.RequestServices.GetService<IValidator<TypeOfInput>>();// i will ask another question for this line
        if(!validator.IsValid(input))
        {
            //send errors
        }
    }

是否可以为上述ActionFilter以及如何编写单元测试?

2 个答案:

答案 0 :(得分:7)

以下是有关如何创建模拟(使用XUnit和Moq框架)以验证调用IsValid方法以及模拟返回false的位置的示例。

using Dealz.Common.Web.Tests.Utils;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.DependencyInjection;
using Moq;
using System;
using Xunit;

namespace Dealz.Common.Web.Tests.ActionFilters
{
    public class TestActionFilter
    {
        [Fact]
        public void ActionFilterTest()
        {
            /****************
             * Setup
             ****************/

            // Create the userValidatorMock
            var userValidatorMock = new Mock<IValidator<User>>();
            userValidatorMock.Setup(validator => validator
                // For any parameter passed to IsValid
                .IsValid(It.IsAny<User>())
            )
            // return false when IsValid is called
            .Returns(false)
            // Make sure that `IsValid` is being called at least once or throw error
            .Verifiable();

            // If provider.GetService(typeof(IValidator<User>)) gets called, 
            // IValidator<User> mock will be returned
            var serviceProviderMock = new Mock<IServiceProvider>();
            serviceProviderMock.Setup(provider => provider.GetService(typeof(IValidator<User>)))
                .Returns(userValidatorMock.Object);

            // Mock the HttpContext to return a mockable 
            var httpContextMock = new Mock<HttpContext>();
            httpContextMock.SetupGet(context => context.RequestServices)
                .Returns(serviceProviderMock.Object);


            var actionExecutingContext = HttpContextUtils.MockedActionExecutingContext(httpContextMock.Object, null);

            /****************
             * Act
             ****************/
            var userValidator = new ValidationActionFilter<User>();
            userValidator.OnActionExecuting(actionExecutingContext);

            /****************
             * Verify
             ****************/

            // Make sure that IsValid is being called at least once, otherwise this throws an exception. This is a behavior test
            userValidatorMock.Verify();

            // TODO: Also Mock HttpContext.Response and return in it's Body proeprty a memory stream where 
            // your ActionFilter writes to and validate the input is what you desire.
        }
    }

    class User
    {
        public string Username { get; set; }
    }

    class ValidationActionFilter<T> : IActionFilter where T : class, new()
    {
        public void OnActionExecuted(ActionExecutedContext context)
        {
            throw new NotImplementedException();
        }

        public void OnActionExecuting(ActionExecutingContext actionContext)
        {
            var type = typeof(IValidator<>).MakeGenericType(typeof(T));

            var validator = (IValidator<T>)actionContext.HttpContext
                .RequestServices.GetService<IValidator<T>>();

            // Get your input somehow
            T input = new T();

            if (!validator.IsValid(input))
            {
                //send errors
                actionContext.HttpContext.Response.WriteAsync("Error");
            }
        }
    }

    internal interface IValidator<T>
    {
        bool IsValid(T input);
    }
}

HttpContextUtils.cs

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using System.Collections.Generic;

namespace Dealz.Common.Web.Tests.Utils
{
    public class HttpContextUtils
    {
        public static ActionExecutingContext MockedActionExecutingContext(
            HttpContext context,
            IList<IFilterMetadata> filters,
            IDictionary<string, object> actionArguments,
            object controller
        )
        {
            var actionContext = new ActionContext() { HttpContext = context };

            return new ActionExecutingContext(actionContext, filters, actionArguments, controller);
        }
        public static ActionExecutingContext MockedActionExecutingContext(
            HttpContext context,
            object controller
        )
        {
            return MockedActionExecutingContext(context, new List<IFilterMetadata>(), new Dictionary<string, object>(), controller);
        }
    }
}

正如您所看到的,它非常混乱,您需要创建大量的模拟来模拟实际类的不同响应,只是为了能够单独测试ActionAttribute。

答案 1 :(得分:7)

我喜欢@Tseng的上述答案,但考虑给出更多答案,因为他的答案涵盖了更多场景(如泛型),并且可能会让一些用户感到压力。

这里我有一个动作过滤器属性,它通过在上下文中设置ModelState属性来检查Result和短路(在没有调用动作的情况下返回响应)请求。在过滤器中,我尝试使用ServiceLocator模式来获取记录器来记录一些数据(有些可能不喜欢这个,但这是一个例子)

过滤

public class ValidationFilterAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext context)
    {
        if (!context.ModelState.IsValid)
        {
            var logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<ValidationFilterAttribute>>();
            logger.LogWarning("some message here");

            context.Result = new JsonResult(new InvalidData() { Message = "some messgae here" })
            {
                StatusCode = 400
            };
        }
    }
}

public class InvalidData
{
    public string Message { get; set; }
}

单元测试

[Fact]
public void ValidationFilterAttributeTest_ModelStateErrors_ResultInBadRequestResult()
{
    // Arrange
    var serviceProviderMock = new Mock<IServiceProvider>();
    serviceProviderMock
        .Setup(serviceProvider => serviceProvider.GetService(typeof(ILogger<ValidationFilterAttribute>)))
        .Returns(Mock.Of<ILogger<ValidationFilterAttribute>>());
    var httpContext = new DefaultHttpContext();
    httpContext.RequestServices = serviceProviderMock.Object;
    var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
    var actionExecutingContext = new ActionExecutingContext(
        actionContext,
        filters: new List<IFilterMetadata>(), // for majority of scenarios you need not worry about populating this parameter
        actionArguments: new Dictionary<string, object>(), // if the filter uses this data, add some data to this dictionary
        controller: null); // since the filter being tested here does not use the data from this parameter, just provide null
    var validationFilter = new ValidationFilterAttribute();

    // Act
    // Add an erorr into model state on purpose to make it invalid
    actionContext.ModelState.AddModelError("Age", "Age cannot be below 18 years.");
    validationFilter.OnActionExecuting(actionExecutingContext);

    // Assert
    var jsonResult = Assert.IsType<JsonResult>(actionExecutingContext.Result);
    Assert.Equal(400, jsonResult.StatusCode);
    var invalidData = Assert.IsType<InvalidData>(jsonResult.Value);
    Assert.Equal("some messgae here", invalidData.Message);
}