简体   繁体   English

如何对装饰有 ServiceFilterAttribute、自定义 ActionFilter 实现的 controller 进行单元测试

[英]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我正在尝试使用ActionFilter实现测试controller
  • Unit test fails, because ActionFilter does not get invoked in unit test.单元测试失败,因为在单元测试中没有调用 ActionFilter。
  • Testing via Postman works as expected and the correct result is achieved.通过 Postman 进行的测试按预期工作,并获得了正确的结果。
  • Can the controller be tested like this or should it move to integration test? controller 可以像这样进行测试还是应该转移到集成测试?

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.我可以在单元测试中自行测试 ActionFilter,我想做的是在单元测试中测试 controller。

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在启动文件中将 ActionFilter 实现添加到服务中

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

The filter can be applied to any controller method that needs to check if an entity exists.过滤器可以应用于需要检查实体是否存在的任何 controller 方法。 ie the GetById method.即GetById 方法。

[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:在 xUnit 测试中,我设置了测试来测试 controller,如下所示:

[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.这就是 InitializeController 方法的样子,我留下了注释行,以便它对我所尝试的内容可见,注释代码都不起作用。 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;我遇到的问题是,在运行单元测试时,ActionFilter 实现从未被调用,因此破坏了测试,因为var entity = HttpContext.Items["entity_found"] as Meeting; in the controller is always null !在 controller 中总是null more accurately HttpContext.Items is always null.更准确地说, HttpContext.Items总是 null。

The break point in the ActionFilter never gets hit. ActionFilter 中的断点永远不会被击中。

When this is tested via postman it all works as expected, and the break point gets hit当通过 postman 进行测试时,一切都按预期工作,并且断点被击中

Is there a way to test the controller as a unit test this way, or should this test now just move to integration ?有没有办法以这种方式将 controller 作为单元测试进行测试,或者该测试现在是否应该转移到集成

Thank you @Fei Han for the link about unit testing controllers .感谢@Fei Han 提供有关单元测试控制器的链接。

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.单元测试控制器设置 controller 动作的单元测试以关注控制器的行为。 A controller unit test avoids scenarios such as filters, routing, and model binding. controller 单元测试避免了过滤器、路由和 model 绑定等场景。 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.这个特殊的场景中,没有GetById(int id)方法的详细实现,对其进行单元测试几乎没有任何价值。

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.如果GetById(int id)方法具有更复杂的实现,或者如果ActionFilter没有阻止进一步处理, HttpContext.Items["entity_found"]

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

相关问题 如何对此自定义命令行开关进行单元测试 - How do I unit test this custom commandlet DotNet Core,如何使用自定义 model 绑定对 Controller 操作方法进行单元测试 - DotNet Core, How do I unit test a Controller Action method with a custom model binding 如何对缓存等实现细节进行单元测试 - How do I unit test an implementation detail like caching c#WebApi如果使用具有特定参数的授权属性修饰控制器操作,如何进行单元测试 - c# WebApi how to unit test if a controller action is decorated with an authorization attribute with specific arguments 如何对自定义DataGridTextColumn实现进行单元测试 - How to unit test a custom DataGridTextColumn implementation 如何在 C# MVC 中将变量从 ActionFilter 传递到控制器操作? - How do I pass a variable from an ActionFilter to a Controller Action in C# MVC? 如何对自定义数据注释执行单元测试 - How to do a Unit Test for a custom Data Annotation 如何对 IDictionary 的实现进行单元测试 - How to Unit Test an Implementation of IDictionary 如何针对仅SSL的Web Api控制器对测试请求进行单元化? - How do I unit test requests against SSL-only Web Api Controller? 如何在ASP.NET MVC中单元测试专用控制器方法? - How do I unit test private controller methods in ASP.NET MVC?
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM