面试编程挑战: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[]>(); } }
我不应修改测试代码,想了解:
- 是否有不通过构造函数注入DbContext的方式?
- 若必须修改测试,该如何解决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

