Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to write unit test for ActionFilter when using Service Locator

I am planning to write an ActionFilter for business validation and in which some services will be resolved via Service Locator(I know this is not good practice and as far as possible i avoid Service Locator pattern, but for this case i want to use it).

OnActionExecuting method of the filter is something like this:

    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
        }
    }

Is it possible to write unit test for above ActionFilterand how?

like image 216
adem caglin Avatar asked Nov 30 '22 16:11

adem caglin


2 Answers

Here is an sample on how to create a mock (using XUnit and Moq framework) to verify that the IsValid method is called and where the mock returns an 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);
        }
    }
}

As you can see, it's quite a mess, you need to create plenty of mocks to simulate different responses of the actuall classes, only to be able to test the ActionAttribute in isolation.

like image 151
Tseng Avatar answered Dec 09 '22 15:12

Tseng


I like @Tseng's above answer but thought of giving one more answer as his answer covers more scenarios (like generics) and could be overwhelming for some users.

Here I have an action filter attribute which just checks the ModelState and short circuits(returns the response without the action being invoked) the request by setting the Result property on the context. Within the filter, I try to use the ServiceLocator pattern to get a logger to log some data(some might not like this but this is an example)

Filter

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; }
}

Unit Test

[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);
}
like image 39
Kiran Avatar answered Dec 09 '22 13:12

Kiran