简体   繁体   中英

How do I unit test a controller that is decorated with the ServiceFilterAttribute, custom ActionFilter implementation

Summary :

  • I'm trying to test a controller with an ActionFilter implementation
  • Unit test fails, because ActionFilter does not get invoked in unit test.
  • Testing via Postman works as expected and the correct result is achieved.
  • Can the controller be tested like this or should it move to integration test?

Breakdown :

I'm able to test the ActionFilter on its own in a unit test, what I would like to do is test the controller in a unit test.

The action filter looks like this:

 public class ValidateEntityExistAttribute<T> : IActionFilter
        where T : class, IEntityBase
    {
        readonly AppDbContext _appDbContext;
        public ValidateEntityExistAttribute(AppDbContext appDbContext)
        {
            this._appDbContext = appDbContext;
        }

        public void OnActionExecuted(ActionExecutedContext context)
        {}

        public void OnActionExecuting(ActionExecutingContext context)
        {
            if (!context.ActionArguments.ContainsKey("id"))
            {
                context.Result = new BadRequestObjectResult("The id must be passed as parameter");
                return;
            }

            int id = (int)context.ActionArguments["id"];

            var foundEntity = _appDbContext.Set<T>().Find(id);
            if (foundEntity == null)
                context.Result = new NotFoundResult();
            else
                context.HttpContext.Items.Add("entity_found", foundEntity);
        }
    }

The ActionFilter implementation is added to the services in the startup file

ConfigureServices(IServiceCollection services)
{
...
 services.AddScoped<ValidateEntityExistAttribute<Meeting>>();
...
}

The filter can be applied to any controller method that needs to check if an entity exists. ie the GetById method.

[HttpGet("{id}")]
[ServiceFilter(typeof(ValidateEntityExistAttribute<Meeting>))]
public async Task<ActionResult<MeetingDto>> GetById(int id)
    {            
        var entity = HttpContext.Items["entity_found"] as Meeting;
        await Task.CompletedTask;
        return Ok(entity.ConvertTo<MeetingDto>());
    }

In the xUnit test I have set up the test to test the controller like this:

[Fact]
public async Task Get_Meeting_Record_By_Id()
{
    // Arrange
    var _AppDbContext = AppDbContextMocker.GetAppDbContext(nameof(Get_All_Meeting_Records));
    var _controller = InitializeController(_AppDbContext);

    //Act
    
    var all = await _controller.GetById(1);

    //Assert
    Assert.Equal(1, all.Value.Id);

    //clean up otherwise the other test will complain about key tracking.
    await _AppDbContext.DisposeAsync();
}

and this is what the InitializeController method look like, I left the commented lines so that it is visible to what I have tried, none of the commented code worked. I mocked and used the default classes.

private MeetingController InitializeController(AppDbContext appDbContext)
    {

        var _controller = new MeetingController(appDbContext);

        var spf = new DefaultServiceProviderFactory(new ServiceProviderOptions { ValidateOnBuild = true, ValidateScopes = true });
        var sc = spf.CreateBuilder(new ServiceCollection());

        sc.AddMvc();
        sc.AddControllers();
        //(config =>
        //{
        //    config.Filters.Add(new ValidateModelStateAttribute());
        //    config.Filters.Add(new ValidateEntityExistAttribute<Meeting>(appDbContext));
        //});
        
        sc.AddTransient<ValidateModelStateAttribute>();
        sc.AddTransient<ValidateEntityExistAttribute<Meeting>>();

        var sp = sc.BuildServiceProvider();

        //var mockHttpContext = new Mock<HttpContext>();
        var httpContext = new DefaultHttpContext
        {
            RequestServices = sp
        };
        //mockHttpContext.Setup(cx => cx.RequestServices).Returns(sp);

        //var contDesc = new ControllerActionDescriptor();
        //var context = new ControllerContext();
        //var context = new ControllerContext(new ActionContext(mockHttpContext.Object, new RouteData(), contDesc));

        //context.HttpContext = mockHttpContext.Object;
        //context.HttpContext = httpContext;
        //_controller.ControllerContext = context;
        _controller.ControllerContext.HttpContext = httpContext;

        return _controller;
    }

The issues I have is that when running the unit test the ActionFilter implementation is never invoked, thus breaking the test because var entity = HttpContext.Items["entity_found"] as Meeting; in the controller is always null ! more accurately HttpContext.Items is always null.

The break point in the ActionFilter never gets hit.

When this is tested via postman it all works as expected, and the break point gets hit

Is there a way to test the controller as a unit test this way, or should this test now just move to integration ?

Thank you @Fei Han for the link about unit testing controllers .

As it turns out this is by design, as stated in the microsoft documentation

Unit testing controllers Set up unit tests of controller actions to focus on the controller's behavior. A controller unit test avoids scenarios such as filters, routing, and model binding. Tests that cover the interactions among components that collectively respond to a request are handled by integration tests.

So the test should move to integration testing.

In this particular scenario , where there is no detailed implementation for the GetById(int id) method, there is almost no value in doing a unit test for it.

If the GetById(int id) method had a more complex implementation, or if the ActionFilter did not prevent further processing, the HttpContext.Items["entity_found"] should be mocked.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM