如何单元测试调用同仓储内两方法的DB Repository方法,避免真实数据库操作
问题分析与解决方案
你遇到的核心问题是:当前Mock的是整个仓储接口,导致测试时调用的是Mock对象的默认InsertOrUpdateWidget实现(返回false),而非WidgetRepository类中的真实逻辑。要测试InsertOrUpdateWidget的分支判断逻辑,同时避免数据库操作,有两种可行方案:
方案一:使用部分Mock(Partial Mock)
直接针对WidgetRepository类创建部分Mock,保留InsertOrUpdateWidget的真实实现,仅Mock它依赖的三个数据操作方法(FindExistingWidget、InsertNewWidget、UpdateExistingWidget)。前提是这三个方法需标记为virtual,允许Mock框架覆盖其行为。
修改仓储类(添加virtual关键字)
public class WidgetRepository { public bool InsertOrUpdateWidget(Widget widgetToEval) { var retval = false; var existingRecord = FindExistingWidget(widgetToEval); if (existingRecord == null) { retval = InsertNewWidget(widgetToEval); } else { retval = UpdateExistingWidget(widgetToEval, existingRecord); } return retval; } // 修改为virtual方法,允许被Mock覆盖 protected virtual Widget FindExistingWidget(Widget widget) { // 原数据库查询逻辑 throw new NotImplementedException(); } protected virtual bool InsertNewWidget(Widget widget) { // 原数据库插入逻辑 throw new NotImplementedException(); } protected virtual bool UpdateExistingWidget(Widget widget, Widget existing) { // 原数据库更新逻辑 throw new NotImplementedException(); } }
编写单元测试
[Fact] public void Insert_New_Widget() { // 创建部分Mock,CallBase=true表示未被Mock的方法调用真实实现 var repoMock = new Mock<WidgetRepository> { CallBase = true }; // Mock依赖方法的行为 repoMock.Setup(s => s.FindExistingWidget(It.IsAny<Widget>())).Returns((Widget)null); repoMock.Setup(s => s.InsertNewWidget(It.IsAny<Widget>())).Returns(true); var newWidget = new Widget() { ID = 1, Name= "test", Description= "Test widget", }; var result = repoMock.Object.InsertOrUpdateWidget(newWidget); Assert.True(result); // 验证InsertNewWidget是否被正确调用 repoMock.Verify(s => s.InsertNewWidget(newWidget), Times.Once); }
方案二:重构代码,抽象底层数据访问
遵循依赖倒置原则,将数据库操作逻辑抽象为独立接口,让WidgetRepository依赖该接口。测试时Mock这个底层接口,完全隔离数据库操作,同时直接实例化真实的WidgetRepository。
抽象数据访问接口
public interface IWidgetDataAccessor { Widget FindExistingWidget(Widget widget); bool InsertNewWidget(Widget widget); bool UpdateExistingWidget(Widget widget, Widget existing); }
修改仓储类,注入依赖
public class WidgetRepository : IWidgetRepository { private readonly IWidgetDataAccessor _dataAccessor; // 通过构造函数注入数据访问依赖 public WidgetRepository(IWidgetDataAccessor dataAccessor) { _dataAccessor = dataAccessor; } public bool InsertOrUpdateWidget(Widget widgetToEval) { var retval = false; var existingRecord = _dataAccessor.FindExistingWidget(widgetToEval); if (existingRecord == null) { retval = _dataAccessor.InsertNewWidget(widgetToEval); } else { retval = _dataAccessor.UpdateExistingWidget(widgetToEval, existingRecord); } return retval; } }
编写单元测试
[Fact] public void Insert_New_Widget() { // Mock底层数据访问接口 var dataAccessorMock = new Mock<IWidgetDataAccessor>(); dataAccessorMock.Setup(d => d.FindExistingWidget(It.IsAny<Widget>())).Returns((Widget)null); dataAccessorMock.Setup(d => d.InsertNewWidget(It.IsAny<Widget>())).Returns(true); // 实例化真实仓储类,传入Mock的依赖 var repo = new WidgetRepository(dataAccessorMock.Object); var newWidget = new Widget() { ID = 1, Name= "test", Description= "Test widget", }; var result = repo.InsertOrUpdateWidget(newWidget); Assert.True(result); // 验证数据访问方法的调用次数和参数 dataAccessorMock.Verify(d => d.FindExistingWidget(newWidget), Times.Once); dataAccessorMock.Verify(d => d.InsertNewWidget(newWidget), Times.Once); }
两种方案对比
- 方案一适合快速修改现有代码,无需大规模重构,但依赖
virtual方法,灵活性较差。 - 方案二更符合面向对象设计原则,代码扩展性和可维护性更强,是长期优化的首选。
内容的提问来源于stack exchange,提问作者dot
相关产品推荐
相关产品推荐

