【问题标题】:How do I unit test a controller that is decorated with the ServiceFilterAttribute, custom ActionFilter implementation如何对装饰有 ServiceFilterAttribute、自定义 ActionFilter 实现的控制器进行单元测试
【发布时间】:2020-10-29 02:45:37
【问题描述】:

总结

  • 我正在尝试使用 ActionFilter 实现来测试 控制器
  • 单元测试失败,因为在单元测试中没有调用 ActionFilter。
  • 通过 Postman 进行的测试按预期工作,并获得了正确的结果。
  • 可以像这样测试控制器还是应该转移到集成测试?

故障

我能够在单元测试中自行测试 ActionFilter,我想做的是在单元测试中测试控制器。

动作过滤器如下所示:

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

ActionFilter 实现添加到启动文件中的服务中

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

过滤器可以应用于任何需要检查实体是否存在的控制器方法。即 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>());
    }

在 xUnit 测试中,我设置了测试来测试控制器,如下所示:

[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();
}

这就是 InitializeController 方法的样子,我留下了注释行,以便它对我所尝试的内容可见,注释代码都不起作用。 我模拟并使用了默认类。

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

问题我遇到的是,在运行单元测试时,ActionFilter 实现永远不会被调用,从而破坏了测试,因为控制器中的var entity = HttpContext.Items["entity_found"] as Meeting; 始终为 null!更准确地说,HttpContext.Items 始终为空。

ActionFilter 中的断点永远不会被命中。

当通过邮递员进行测试时,一切都按预期工作,并且断点被命中

有没有办法以这种方式将控制器作为单元测试进行测试,或者这个测试现在应该转移到集成中

【问题讨论】:

  • 请注意controller unit test 避免了过滤器、路由和模型绑定等场景。对于单元测试,您不需要在特定控制器操作上测试过滤器,您可以测试过滤器本身。
  • 谢谢@Fei Han!这确实说明了为什么不触发过滤器。我想,有了这个小实现的控制器,为它编写单元测试几乎没有任何价值。

标签: c# unit-testing asp.net-core .net-core custom-action-filter


【解决方案1】:

感谢@Fei Han 提供链接about unit testing controllers

事实证明这是设计使然,如 microsoft 文档中所述

单元测试控制器 设置控制器动作的单元测试 关注控制器的行为。控制器单元测试避免 过滤器、路由和模型绑定等场景。测试 涵盖共同响应的组件之间的交互 请求由集成测试处理。

所以测试应该转移到集成测试。

这个特定的场景中,GetById(int id) 方法没有详细的实现,因此为它进行单元测试几乎没有任何价值。 p>

如果GetById(int id) 方法有更复杂的实现,或者如果ActionFilter 没有阻止进一步处理,则应该模拟HttpContext.Items["entity_found"]

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2012-01-20
    • 2015-09-27
    • 2014-12-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-07-12
    • 2019-03-21
    相关资源
    最近更新 更多