RequestContext.Principal 的 Autofac DI 在单元测试中使用 WebAPI2
Autofac DI for RequestContext.Principal using WebAPI2 in a unit test
我在一个从头开始构建的 WebAPI 项目中使用 Autofac 和 OWIN(与 VS2015 中可用的完整 WebAPI 模板相对)。不可否认,我是这样做的新手。
在单元测试项目中,我在单元测试开始时设置了一个OWIN启动class:
WebApp.Start<Startup>("http://localhost:9000/")
启动class如下:
[assembly: OwinStartup(typeof(API.Specs.Startup))]
namespace API.Specs
{
public class Startup
{
public void Configuration(IAppBuilder appBuilder)
{
var config = new HttpConfiguration();
//config.Filters.Add(new AccessControlAttribute());
config.Services.Replace(typeof(IAssembliesResolver), new CustomAssembliesResolver());
config.Formatters.JsonFormatter.SerializerSettings = Serializer.Settings;
config.MapHttpAttributeRoutes();
// Autofac configuration
var builder = new ContainerBuilder();
// Unit of Work
var unitOfWork = new Mock<IUnitOfWork>();
builder.RegisterInstance(unitOfWork.Object).As<IUnitOfWork>();
// Principal
var principal = new Mock<IPrincipal>();
principal.Setup(p => p.IsInRole("admin")).Returns(true);
principal.SetupGet(p => p.Identity.Name).Returns('test.user');
principal.SetupGet(p => p.Identity.IsAuthenticated).Returns(true);
Thread.CurrentPrincipal = principal.Object;
if (HttpContext.Current != null)
{
HttpContext.Current.User = new GenericPrincipal(principal.Object.Identity, null);
}
builder.Register(c => principal).As<IPrincipal>();
.
.
.
// Set up dependencies for Controllers, Services & Repositories
.
.
.
var container = builder.Build();
config.DependencyResolver = new AutofacWebApiDependencyResolver(container);
config.IncludeErrorDetailPolicy = IncludeErrorDetailPolicy.Always;
appBuilder.UseWebApi(config);
}
private static void RegisterAssemblies<TModel, TController, TService, TRepoClass, TRepoInterface>(ref ContainerBuilder builder, ref Mock<IUnitOfWork> unitOfWork)
where TModel : class
where TRepoClass : class
where TService : class
{
RegisterController<TController>(ref builder);
var repositoryInstance = RegisterRepository<TRepoClass, TRepoInterface>(ref builder);
RegisterService<TService>(ref builder, ref unitOfWork, repositoryInstance);
}
private static void RegisterController<TController>(ref ContainerBuilder builder)
{
builder.RegisterApiControllers(typeof(TController).Assembly);
}
private static object RegisterRepository<TRepoClass, TRepoInterface>(ref ContainerBuilder builder)
where TRepoClass : class
{
var constructorArguments = new object[] { DataContexts.Instantiate };
var repositoryInstance = Activator.CreateInstance(typeof(TRepoClass), constructorArguments);
builder.RegisterInstance(repositoryInstance).As<TRepoInterface>();
return repositoryInstance;
}
private static void RegisterService<TService>(ref ContainerBuilder builder, ref Mock<IUnitOfWork> unitOfWork, object repositoryInstance)
where TService : class
{
var constructorArguments = new[] { repositoryInstance, unitOfWork.Object};
var serviceInstance = Activator.CreateInstance(typeof(TService), constructorArguments);
builder.RegisterAssemblyTypes(typeof(TService).Assembly)
.Where(t => t.Name.EndsWith("Service"))
.AsImplementedInterfaces().InstancePerRequest();
builder.RegisterInstance(serviceInstance);
}
}
}
旁注:理想情况下,我想将 Principle 设置为测试的一部分,以便能够将不同的用户传递给控制器,但如果我绝对必须保留 CurrentPrincipal/User 在启动 class 中,我可以解决它。
启动 class 工作正常 w.r.t 使用 DI 访问我的控制器,但是从未设置 RequestContext.Principal
中的主体。它始终为空。我打算使用请求上下文的方式如下:
[HttpGet]
[Route("path/{testId}")]
[ResponseType(typeof(Test))]
public IHttpActionResult Get(string testId)
{
return Ok(_service.GetById(testId, RequestContext.Principal.Identity.Name));
}
我还尝试将模拟主体 class 注入到我的 Controller 的构造函数中作为解决方法 - 我使用了与通用方法中显示的相同方法来使用 DI 设置我的服务。然而,我再次在我的构造函数中得到了空值。
在这一点上,我已经为这个问题坐了大约一天,拔掉了我的头发。任何帮助,将不胜感激。提前致谢。
我会避免使用 DI 执行此操作。 您需要在请求上下文中设置主体,而不是将主体注入构造函数。
如果是我,我会这样做:
首先,我不会模拟不需要模拟的东西。也就是说,您的 IIdentity
实现实际上可能是真实对象。
private static IPrincipal CreatePrincipal()
{
var identity = new GenericIdentity("test.user", "test");
var roles = new string[] { "admin" };
return new GenericPrincipal(identity);
}
接下来,您需要 运行 在您通过测试应用程序处理的每个 "request" 上进行设置。 我猜这是更多 "integration test" 比 "unit test" 因为你使用的是整个启动 class 和所有东西,所以你不能只设置一次主体就完成了。它必须在每个请求上完成,就像真正的身份验证操作一样。
最简单的方法是使用 a simple delegating handler。
public class TestAuthHandler : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken)
{
// Set the principal. Whether you set the thread principal
// is optional but you should really use the request context
// principal exclusively when checking permissions.
request.GetRequestContext().Principal = CreatePrincipal();
// Let the request proceed through the rest of the pipeline.
return await base.SendAsync(request, cancellationToken);
}
}
最后,将该处理程序添加到 Startup
class.
中的 HttpConfiguration
管道
config.MessageHandlers.Add(new TestAuthHandler());
应该可以了。请求现在应该通过该身份验证处理程序并分配主体。
我在一个从头开始构建的 WebAPI 项目中使用 Autofac 和 OWIN(与 VS2015 中可用的完整 WebAPI 模板相对)。不可否认,我是这样做的新手。
在单元测试项目中,我在单元测试开始时设置了一个OWIN启动class:
WebApp.Start<Startup>("http://localhost:9000/")
启动class如下:
[assembly: OwinStartup(typeof(API.Specs.Startup))]
namespace API.Specs
{
public class Startup
{
public void Configuration(IAppBuilder appBuilder)
{
var config = new HttpConfiguration();
//config.Filters.Add(new AccessControlAttribute());
config.Services.Replace(typeof(IAssembliesResolver), new CustomAssembliesResolver());
config.Formatters.JsonFormatter.SerializerSettings = Serializer.Settings;
config.MapHttpAttributeRoutes();
// Autofac configuration
var builder = new ContainerBuilder();
// Unit of Work
var unitOfWork = new Mock<IUnitOfWork>();
builder.RegisterInstance(unitOfWork.Object).As<IUnitOfWork>();
// Principal
var principal = new Mock<IPrincipal>();
principal.Setup(p => p.IsInRole("admin")).Returns(true);
principal.SetupGet(p => p.Identity.Name).Returns('test.user');
principal.SetupGet(p => p.Identity.IsAuthenticated).Returns(true);
Thread.CurrentPrincipal = principal.Object;
if (HttpContext.Current != null)
{
HttpContext.Current.User = new GenericPrincipal(principal.Object.Identity, null);
}
builder.Register(c => principal).As<IPrincipal>();
.
.
.
// Set up dependencies for Controllers, Services & Repositories
.
.
.
var container = builder.Build();
config.DependencyResolver = new AutofacWebApiDependencyResolver(container);
config.IncludeErrorDetailPolicy = IncludeErrorDetailPolicy.Always;
appBuilder.UseWebApi(config);
}
private static void RegisterAssemblies<TModel, TController, TService, TRepoClass, TRepoInterface>(ref ContainerBuilder builder, ref Mock<IUnitOfWork> unitOfWork)
where TModel : class
where TRepoClass : class
where TService : class
{
RegisterController<TController>(ref builder);
var repositoryInstance = RegisterRepository<TRepoClass, TRepoInterface>(ref builder);
RegisterService<TService>(ref builder, ref unitOfWork, repositoryInstance);
}
private static void RegisterController<TController>(ref ContainerBuilder builder)
{
builder.RegisterApiControllers(typeof(TController).Assembly);
}
private static object RegisterRepository<TRepoClass, TRepoInterface>(ref ContainerBuilder builder)
where TRepoClass : class
{
var constructorArguments = new object[] { DataContexts.Instantiate };
var repositoryInstance = Activator.CreateInstance(typeof(TRepoClass), constructorArguments);
builder.RegisterInstance(repositoryInstance).As<TRepoInterface>();
return repositoryInstance;
}
private static void RegisterService<TService>(ref ContainerBuilder builder, ref Mock<IUnitOfWork> unitOfWork, object repositoryInstance)
where TService : class
{
var constructorArguments = new[] { repositoryInstance, unitOfWork.Object};
var serviceInstance = Activator.CreateInstance(typeof(TService), constructorArguments);
builder.RegisterAssemblyTypes(typeof(TService).Assembly)
.Where(t => t.Name.EndsWith("Service"))
.AsImplementedInterfaces().InstancePerRequest();
builder.RegisterInstance(serviceInstance);
}
}
}
旁注:理想情况下,我想将 Principle 设置为测试的一部分,以便能够将不同的用户传递给控制器,但如果我绝对必须保留 CurrentPrincipal/User 在启动 class 中,我可以解决它。
启动 class 工作正常 w.r.t 使用 DI 访问我的控制器,但是从未设置 RequestContext.Principal
中的主体。它始终为空。我打算使用请求上下文的方式如下:
[HttpGet]
[Route("path/{testId}")]
[ResponseType(typeof(Test))]
public IHttpActionResult Get(string testId)
{
return Ok(_service.GetById(testId, RequestContext.Principal.Identity.Name));
}
我还尝试将模拟主体 class 注入到我的 Controller 的构造函数中作为解决方法 - 我使用了与通用方法中显示的相同方法来使用 DI 设置我的服务。然而,我再次在我的构造函数中得到了空值。
在这一点上,我已经为这个问题坐了大约一天,拔掉了我的头发。任何帮助,将不胜感激。提前致谢。
我会避免使用 DI 执行此操作。 您需要在请求上下文中设置主体,而不是将主体注入构造函数。
如果是我,我会这样做:
首先,我不会模拟不需要模拟的东西。也就是说,您的 IIdentity
实现实际上可能是真实对象。
private static IPrincipal CreatePrincipal()
{
var identity = new GenericIdentity("test.user", "test");
var roles = new string[] { "admin" };
return new GenericPrincipal(identity);
}
接下来,您需要 运行 在您通过测试应用程序处理的每个 "request" 上进行设置。 我猜这是更多 "integration test" 比 "unit test" 因为你使用的是整个启动 class 和所有东西,所以你不能只设置一次主体就完成了。它必须在每个请求上完成,就像真正的身份验证操作一样。
最简单的方法是使用 a simple delegating handler。
public class TestAuthHandler : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken)
{
// Set the principal. Whether you set the thread principal
// is optional but you should really use the request context
// principal exclusively when checking permissions.
request.GetRequestContext().Principal = CreatePrincipal();
// Let the request proceed through the rest of the pipeline.
return await base.SendAsync(request, cancellationToken);
}
}
最后,将该处理程序添加到 Startup
class.
HttpConfiguration
管道
config.MessageHandlers.Add(new TestAuthHandler());
应该可以了。请求现在应该通过该身份验证处理程序并分配主体。