asp.net-core-mvcactionfilterattribute

How to unit test an action filter attribute for web api in asp.net core?


I have written an action filter for a web api. If a method in the api controller throws an unhandled exception, then the filter creates an internal error 500 response.

I need to know how to test the filter?

I have researched extensively but could not create a suitable test. I tried context mocking, a service locator implementation and even an integration test using a test server.

The web api controller looks like this:

namespace Plod.Api.ApiControllers
{
    [TypeFilter(typeof(UnhandledErrorFilterAttribute))]
    [Route("api/[controller]")]
    public class GamesController : BaseApiController
    {
        public GamesController(IGameService repository, 
            ILogger<GamesController> logger,
            IGameFactory gameFactory
            ) : base(
                repository, 
                logger,
                gameFactory
                )
        { }

        // ..... controller methods are here
    }
}

The complete controller is found here.

The filter is this:

namespace Plod.Api.Filters
{
    public class UnhandledErrorFilterAttribute : ActionFilterAttribute
    {
        public override void OnActionExecuted(ActionExecutedContext filterContext)
        {
            if (filterContext.Exception != null)
            {
                filterContext.HttpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
                filterContext.ExceptionHandled = true;
            }
        }
    }
}

I even welcome changes to the filter implementation as a possible work around. Any help or ideas would be much appreciated. Thanks.


Solution

  • You probably can't. However, what you can do is spin up a TestServer and then hit it with a HttpClient. This really is an integration test and not a unit test. However, it's the good kind of integration test because it can be run safely in pipelines.

    This document explains how to do this: https://learn.microsoft.com/en-us/aspnet/core/test/integration-tests?view=aspnetcore-3.1

    The issue you are going to face is that you will need to mock the underlying services inside your app. If you don't do that, your whole server will spin up and attempt to hit the database etc. Here is an example. This is using Moq. Incidentally I am sharing the ConfigureServices method with unit tests so they use the same object mesh of mocked services. You can still use the full functionality of Moq or NSubstitute to test the back-end (or even front -end).

    I can hit my attributes in the test with breakpoint.

    private void ConfigureServices(IServiceCollection services)
    {
        var hostBuilder = new WebHostBuilder();
        hostBuilder.UseStartup<TestStartup>();
        hostBuilder.ConfigureServices(services =>
        {
            ConfigureServices(services);
        });
    
        _testServer = new TestServer(hostBuilder);
        _httpClient = _testServer.CreateClient();
    }
    
    
    private void ConfigureServices(IServiceCollection services)
    {
        services.AddSingleton(_storageManagerFactory.Object);
        services.AddSingleton(_blobReferenceManagerMock.Object);
        services.AddSingleton(_ipActivitiesLoggerMocker.Object);
        services.AddSingleton(_loggerFactoryMock.Object);
        services.AddSingleton(_hashingService);
        services.AddSingleton(_settingsServiceMock.Object);
        services.AddSingleton(_ipActivitiesManager.Object);
        services.AddSingleton(_restClientMock.Object);
        _serviceProvider = services.BuildServiceProvider();
    }
    
    
    public class TestStartup 
    {
        public void Configure(
            IApplicationBuilder app,
            ISettingsService settingsService)
        {
            app.Configure(settingsService.GetSettings());
        }
    
        public IServiceProvider ConfigureServices(IServiceCollection services)
        {
            var mvc = services.AddMvc(option => option.EnableEndpointRouting = false);
            mvc.AddApplicationPart(typeof(BlobController).Assembly);
            services.AddSingleton(new Mock<IHttpContextAccessor>().Object);
            return services.BuildServiceProvider();
        }
    }