You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

集成测试中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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.20 23:07:04