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

面试编程挑战:DbContext依赖注入与单元测试兼容难题

问题描述

我知道这个问题类似很多已有问题,但实在搞不清是自己忽略了明显点还是其他原因,需要前辈指点。我在面试编程挑战中遇到如下问题:

提供的代码可编译运行但实际无法工作,以下是ExampleController的代码片段(已替换为通用名称):

public class ExampleController : BaseApiController
{
    private readonly SomeRepository _repository;

    [HttpGet]
    [ProducesResponseType(StatusCodes.Status200OK)]
    public async Task<ActionResult> GetAll()
    {
        var items = await _repository.Items.GetAllAsync();
        return Ok(items);
    }
}

BaseApiController代码如下:

[Route("api/[controller]")]
public class BaseApiController : ControllerBase
{ }

显然_repository未初始化,我添加了接收SomeDbContext的构造函数来初始化_repository,修改后的代码如下:

public class ExampleController : BaseApiController
{
    private readonly SomeRepository _repository;

    public ExampleController(SomeDbContext context){
        _repository = new SomeRepository(context);
    }

    [HttpGet]
    [ProducesResponseType(StatusCodes.Status200OK)]
    public async Task<ActionResult> GetAll()
    {
        var items = await _repository.Items.GetAllAsync();
        return Ok(items);
    }
}

修改后功能正常,可通过Postman获取数据,但破坏了所有给定的单元测试。原测试使用无参构造函数初始化ExampleController,并通过Mock<ISomeRepository>模拟依赖,示例测试代码如下:

public class ExampleControllerTests
{
    private readonly Mock<ISomeRepository> _repository;

    public ExampleControllerTests()
    {
        this._repository = new Mock<ISomeRepository>();
    }

[Fact]
    public async Task Get_OnSuccess_ReturnsListOfItems()
    {
        Item[] items = ItemFixture.GetItemsMock().ToArray();

        this._repository
            .Setup(x => x.Items.GetAllAsync())
            .ReturnsAsync(items);

        ExampleController sut = new ExampleController();

        ActionResult result = await sut.GetAll();
        OkObjectResult objectResults = (OkObjectResult) result;
        objectResults?.Value.Should().BeOfType<Item[]>();
    }
}

我不应修改测试代码,想了解:

  1. 是否有不通过构造函数注入DbContext的方式?
  2. 若必须修改测试,该如何解决DbContext需要DbContextOptions的问题?
解决方案

一、不通过构造函数注入DbContext的可行方案

如果不能修改测试代码,核心是让控制器同时支持无参构造(供测试用)和依赖DbContext的构造(供生产用),同时保证_repository能正确初始化:

方案1:添加无参构造函数,兼容测试依赖

把_repository的类型改为ISomeRepository(原测试用的是接口模拟,这是关键),然后添加无参构造,内部初始化Mock实例:

public class ExampleController : BaseApiController
{
    private readonly ISomeRepository _repository;

    // 生产环境构造
    public ExampleController(SomeDbContext context)
    {
        _repository = new SomeRepository(context);
    }

    // 测试用无参构造
    public ExampleController()
    {
        // 生成符合测试逻辑的Mock实例
        _repository = new Mock<ISomeRepository>().Object;
    }

    [HttpGet]
    [ProducesResponseType(StatusCodes.Status200OK)]
    public async Task<ActionResult> GetAll()
    {
        var items = await _repository.Items.GetAllAsync();
        return Ok(items);
    }
}

这样原测试的无参构造调用就能正常工作,生产环境也能通过DbContext初始化真实仓储。

方案2:属性注入(临时 workaround,不推荐)

ASP.NET Core支持属性注入,将_repository改为可设置的属性,生产环境通过DI容器配置注入,测试时可直接赋值:

public class ExampleController : BaseApiController
{
    [Inject]
    public ISomeRepository _repository { get; set; }

    // 保留无参构造
    public ExampleController(){}

    // 可选:生产用构造兜底
    public ExampleController(SomeDbContext context)
    {
        _repository = new SomeRepository(context);
    }

    // GetAll方法不变
}

注意:属性注入不符合依赖注入的最佳实践,耦合度更高,仅作为临时解决手段。

二、必须修改测试时的解决方案

如果允许调整测试代码,有两种主流方案:

方案1:让控制器依赖抽象仓储,直接注入Mock实例

修改控制器构造函数,直接依赖ISomeRepository而非SomeDbContext:

public class ExampleController : BaseApiController
{
    private readonly ISomeRepository _repository;

    public ExampleController(ISomeRepository repository)
    {
        _repository = repository;
    }

    // GetAll方法不变
}

然后修改测试代码,把Mock实例传入控制器构造:

ExampleController sut = new ExampleController(_repository.Object);

这是最符合依赖注入原则的方式,控制器依赖抽象而非具体实现,测试和生产环境都能通过DI注入对应实例。

方案2:创建内存数据库的DbContext实例

如果必须保留控制器依赖SomeDbContext的构造,可以在测试中创建使用内存数据库的SomeDbContext,避免依赖真实数据库:

// 初始化内存数据库配置
var options = new DbContextOptionsBuilder<SomeDbContext>()
    .UseInMemoryDatabase(databaseName: "Test_ExampleDb")
    .Options;

// 创建DbContext并添加测试数据
var context = new SomeDbContext(options);
context.Items.AddRange(ItemFixture.GetItemsMock());
context.SaveChanges();

// 初始化控制器
ExampleController sut = new ExampleController(context);

这种方式更适合集成测试,单元测试更推荐直接模拟仓储接口,减少对EF Core实现的依赖。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.27 16:27:51