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

如何单元测试调用同仓储内两方法的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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.13 09:40:25