集成测试中Scoped服务实例不一致问题求助
ASP.NET Core集成测试:Scoped服务初始化不生效问题解决
问题概述
编写ASP.NET Core集成测试时,对Scoped服务(实际为Microsoft.AspNetCore.Identity.UserManager<User>)的私有字段进行初始化后,控制器中使用的服务实例未体现该初始化效果,导致测试失败。测试代码中获取的服务实例与控制器注入的实例并非同一个。
场景代码示例
第三方不可修改的Scoped服务
interface IScopedService { bool IsFooValid(int foo); } class ScopedService { private List<int> _Foo = new(); public void Register(int foo) { _Foo.Add(foo); } public bool IsFooValid(int foo) { return _Foo.Contains(foo); } }
控制器代码
class MyController(IScopedService scopedServiceInController) : ApiController { [HttpPost] public bool MyFunc(int foo) { return scopedServiceInController.IsFooValid(foo); } }
客户端调用代码
class MyEndpoints(HttpClient httpClient) { public async Task<bool?> MyFunc(int foo) { var request = new HttpRequestMessage(HttpMethod.Post, "path") { Content = JsonContent.Create(foo) }; var result = await httpClient.SendAsync(request); if (!result.IsSuccessStatusCode) { return null; } return await result.Content.ReadFromJson<bool>(); } }
当前集成测试代码
class MyWebApplicationFactory: WebApplicationFactory<Program>, IAsyncLifetime { public IServiceProvider ScopedServiceProvider => _Scope.ServiceProvider; private IServiceScope _Scope; public virtual async Task InitializeAsync() { _Scope = Services.CreateScope(); } public new virtual async Task DisposeAsync() { _Scope.Dispose(); } } class MyServiceTests: IClassFixture<MyWebApplicationFactory> { private readonly MyWebApplicationFactory _Factory; private readonly MyEndpoints _Sut; public MyServiceTests(MyWebApplicationFactory factory) { _Factory = factory; var client = factory.CreateClient(); _Sut = new MyEndpoints(client); } [Fact] public async Task Test() { // Arrange var scopedServiceInTest = _Factory.ScopedServiceProvider.GetRequiredService<ScopedService>(); scopedServiceInTest.Register(1); // Act var actual = await _Sut.MyFunc(1); // Assert actual.Should().BeTrue(); } }
补充服务注册与控制器代码
public static void Register(this IServiceCollection services, IConfiguration configuration) { services.AddIdentity<User, Role>( o => { // identity配置(无关代码) } ) .AddEntityFrameworkStores<MyIdentityDbContext>() .AddDefaultTokenProviders() .AddUserManager<MyUserManager>() .AddRoleManager<MyRoleManager>(); services.AddControllers(); services.AddScoped<IRoleManager>(o => o.GetRequiredService<MyRoleManager>()); services.AddScoped<IUserManager>(o => o.GetRequiredService<MyUserManager>()); } class MyIdentityDbContext : IdentityDbContext<User, Role, string>; class MyUserManager: UserManager<User>, IUserManager; class MyRoleManager: RoleManager<Role>, IRoleManager;
[ApiController] [Route("[controller]")] public sealed class UserController(IUserManager userManager) : ControllerBase { // 控制器逻辑 }
核心原因
测试代码中通过Services.CreateScope()创建的Scope,与ASP.NET Core处理请求时自动创建的请求级Scope是完全独立的两个上下文。因此测试中初始化的服务实例,和控制器在请求处理时从自身Scope获取的实例不是同一个,初始化操作无法作用到请求流程中。
解决方案
方法一:利用请求中间件在当前Scope中初始化服务
通过在WebApplicationFactory中配置自定义中间件,在每个请求处理前获取当前请求Scope内的服务实例,执行初始化操作。这种方式能确保初始化的是控制器实际使用的实例。
修改MyWebApplicationFactory:
class MyWebApplicationFactory : WebApplicationFactory<Program> { // 存储测试用例需要初始化的值 private int _targetFoo; protected override void ConfigureWebHost(IWebHostBuilder builder) { builder.Configure(app => { // 在请求管道中插入自定义中间件 app.Use(async (context, next) => { // 获取当前请求Scope的ScopedService实例 var scopedService = context.RequestServices.GetRequiredService<ScopedService>(); scopedService.Register(_targetFoo); // 继续执行后续中间件和控制器逻辑 await next(); }); }); } // 提供方法给测试用例设置初始化参数 public void SetInitialFoo(int foo) { _targetFoo = foo; } }
修改测试用例:
[Fact] public async Task Test() { // Arrange _Factory.SetInitialFoo(1); // Act var actual = await _Sut.MyFunc(1); // Assert actual.Should().BeTrue(); }
针对UserManager的特殊处理
如果是UserManager<User>这类Identity服务,需要初始化私有字段时,可以通过反射在中间件中完成:
app.Use(async (context, next) => { var userManager = context.RequestServices.GetRequiredService<IUserManager>(); // 获取私有字段并设置值 var privateField = userManager.GetType() .GetField("_privateFieldName", BindingFlags.NonPublic | BindingFlags.Instance); privateField?.SetValue(userManager, yourInitializedValue); await next(); });
方法二:替换Scoped服务为可控制实例(可选)
如果允许调整服务注册逻辑,可以在测试中替换原Scoped服务为提前初始化好的实例:
protected override void ConfigureWebHost(IWebHostBuilder builder) { builder.ConfigureServices(services => { // 移除原有的IUserManager注册 var descriptor = services.SingleOrDefault(d => d.ServiceType == typeof(IUserManager)); if (descriptor != null) { services.Remove(descriptor); } // 创建并初始化自定义UserManager实例 var userManager = new MyUserManager(/* 构造参数,可从原服务容器获取依赖 */); var privateField = userManager.GetType() .GetField("_privateFieldName", BindingFlags.NonPublic | BindingFlags.Instance); privateField?.SetValue(userManager, yourInitializedValue); // 注册为Scoped服务(确保每个请求使用同一个实例) services.AddScoped<IUserManager>(_ => userManager); }); }
这种方式适合需要复用同一个初始化实例的场景,但需注意UserManager的构造参数可能较多,需要妥善处理依赖注入。
内容的提问来源于stack exchange,提问作者CzBuCHi
相关产品推荐
相关产品推荐

