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

ASP.NET Core Web API单元测试中DbContext条件注册失效问题

问题描述

使用C#开发ASP.NET Core Web API时,在NUnit测试项目中模拟Web应用遇到错误:服务提供者中同时注册了Microsoft.EntityFrameworkCore.SqlServer与Microsoft.EntityFrameworkCore.InMemory数据库提供者,仅允许注册单个。已在Program类中通过IsUnitTest配置项编写条件注册逻辑,但测试时该配置项取值不符合预期(返回null),尝试移除服务描述符也无效,现需实现仅在非测试环境注册SqlServer的DbContext。

Program类代码
public partial class Program
{
    public static void Main(string[] args)
    {
        var builder = WebApplication.CreateBuilder(args);
        var logger = LoggerFactory.Create(logging => logging.AddConsole()).CreateLogger<Program>();
        var isUnitTest = builder.Configuration.GetValue<bool>("IsUnitTest");
        
        logger.LogInformation("IsUnitTest: {IsUnitTest}", isUnitTest);

        if (isUnitTest)
        {
            builder.Services.AddDbContext<API.Context.APIContext>(options => options.UseInMemoryDatabase("InMemoryDbForTesting"));
        }
        else
        {
            var conString = builder.Configuration.GetConnectionString("DatabaseConnection") ??
                throw new InvalidOperationException("Connection string 'DatabaseConnection' not found.");
            builder.Services.AddDbContext<API.Context.APIContext>(options => options.UseSqlServer(conString));
        }

        // Add services to the container.
        builder.Services.AddControllers();

        // Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
        builder.Services.AddOpenApi();
        builder.Services.AddEndpointsApiExplorer();
        builder.Services.AddSwaggerGen();

        var app = builder.Build();

        // Configure the HTTP request pipeline.
        if (app.Environment.IsDevelopment())
        {
            app.MapOpenApi();
        }

        if (app.Environment.IsDevelopment())
        {
            app.UseSwagger();
            app.UseSwaggerUI();
        };

        app.UseHttpsRedirection();
        app.UseAuthorization();
        app.MapControllers();
        app.MapServiceEndpoints();
        app.MapScanEndpoints();
        app.MapPackageEndpoints();
        app.MapImageEndpoints();

        app.Run();
    }
}
单元测试代码
private WebApplicationFactory<Program>? _factory;
private HttpClient? _client;

[SetUp]
public void Setup()
{
    _factory = new WebApplicationFactory<Program>()
        .WithWebHostBuilder(builder =>
        {
            builder.ConfigureAppConfiguration((context, config) =>
            {
                var settings = new Dictionary<string, string>
                {
                    { "IsUnitTest", "true" }
                };
                config.AddInMemoryCollection(settings);
            });

            builder.ConfigureServices(services =>
            {
                // Remove all DbContextOptions<APIContext> descriptors
                var descriptors = services.Where(d => d.ServiceType == typeof(DbContextOptions<APIContext>)).ToList();
                foreach (var descriptor in descriptors)
                {
                    services.Remove(descriptor);
                }

                services.AddDbContext<APIContext>(options =>
                {
                    options.UseInMemoryDatabase("InMemoryDbForTesting");
                });

                // Add logging
                services.AddLogging(configure => configure.AddConsole());
            });
        });

    _client = _factory.CreateClient();
}
解决方案

方案1:修复配置项读取问题,确保测试配置优先

测试中添加的IsUnitTest配置被原有配置源覆盖,导致Program中读取不到正确值。修改测试代码的配置逻辑,清除原有配置源后再添加测试配置:

builder.ConfigureAppConfiguration((context, config) =>
{
    config.Sources.Clear(); // 清除默认配置源,确保测试配置优先
    var settings = new Dictionary<string, string>
    {
        { "IsUnitTest", "true" }
    };
    config.AddInMemoryCollection(settings);
});

这样Program中的isUnitTest会正确读取到true,自动注册InMemory数据库,不会触发SqlServer的注册逻辑,从根源避免冲突。

方案2:彻底移除原有DbContext注册,重新添加测试用实例

如果不想修改配置逻辑,可在测试中彻底移除所有与APIContext相关的服务注册,再重新添加InMemory版本:

builder.ConfigureServices(services =>
{
    // 移除APIContext本身及对应的DbContextOptions配置
    var descriptors = services.Where(d => 
        d.ServiceType == typeof(APIContext) || 
        d.ServiceType == typeof(DbContextOptions<APIContext>)).ToList();
    foreach (var descriptor in descriptors)
    {
        services.Remove(descriptor);
    }

    // 重新注册InMemory数据库的DbContext
    services.AddDbContext<APIContext>(options =>
    {
        options.UseInMemoryDatabase("InMemoryDbForTesting");
    });

    services.AddLogging(configure => configure.AddConsole());
});

方案3:使用环境标识替代配置项(更简洁)

在Program中通过环境判断替代配置项,测试时指定测试环境:

修改Program类的DbContext注册逻辑

// 替换原来的IsUnitTest判断
if (builder.Environment.IsEnvironment("Testing"))
{
    builder.Services.AddDbContext<API.Context.APIContext>(options => options.UseInMemoryDatabase("InMemoryDbForTesting"));
}
else
{
    var conString = builder.Configuration.GetConnectionString("DatabaseConnection") ??
        throw new InvalidOperationException("Connection string 'DatabaseConnection' not found.");
    builder.Services.AddDbContext<API.Context.APIContext>(options => options.UseSqlServer(conString));
}

修改测试代码,指定测试环境

_factory = new WebApplicationFactory<Program>()
    .WithWebHostBuilder(builder =>
    {
        builder.UseEnvironment("Testing"); // 设置测试环境

        builder.ConfigureServices(services =>
        {
            services.AddLogging(configure => configure.AddConsole());
        });
    });

这种方式无需配置项,直接通过环境标识区分测试与生产环境,逻辑更清晰。


内容的提问来源于stack exchange,提问作者Hiromi

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 11:12:36