如何对用 ServiceFilterAttribute 自定义 ActionFilter 实现装饰的控制器进行单元测试
How do I unit test a controller that is decorated with the ServiceFilterAttribute, custom ActionFilter implementation
总结:
- 我正在尝试使用 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 方法的样子,我留下了注释行,以便它对我尝试的内容可见,none 注释代码有效。
我嘲笑并使用默认 类.
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 中的断点。
当通过邮递员进行测试时,一切都按预期工作,并且遇到了断点
有没有办法以这种方式将控制器作为单元测试进行测试,或者现在应该将此测试转移到集成?
感谢@Fei Han link about unit testing controllers.
事实证明这是设计使然,如 Microsoft 文档中所述
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.
所以测试应该转到集成测试。
在这个特定场景中,GetById(int id)
方法没有详细的实现,单元测试几乎没有价值 为之。
如果 GetById(int id)
方法有更复杂的实现,或者如果 ActionFilter 没有阻止进一步的处理,那么 HttpContext.Items["entity_found"]
应该被模拟。
总结:
- 我正在尝试使用 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 方法的样子,我留下了注释行,以便它对我尝试的内容可见,none 注释代码有效。 我嘲笑并使用默认 类.
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 中的断点。
当通过邮递员进行测试时,一切都按预期工作,并且遇到了断点
有没有办法以这种方式将控制器作为单元测试进行测试,或者现在应该将此测试转移到集成?
感谢@Fei Han link about unit testing controllers.
事实证明这是设计使然,如 Microsoft 文档中所述
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.
所以测试应该转到集成测试。
在这个特定场景中,GetById(int id)
方法没有详细的实现,单元测试几乎没有价值 为之。
如果 GetById(int id)
方法有更复杂的实现,或者如果 ActionFilter 没有阻止进一步的处理,那么 HttpContext.Items["entity_found"]
应该被模拟。