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

如何在C#工作单元项目中对含模拟仓储的服务做单元测试?

高效测试基于Clean架构与Unit of Work模式的Service

问题背景

项目基于Clean架构搭建,基础设施层采用Unit of Work模式,使用MSTest和Moq4编写单元测试时,遇到以下问题:

  • Service依赖的IUnitOfWork实例化过程繁琐,手动构造真实UnitOfWork并传入模拟DbContext的方式效率低下
  • GenericService包含多个依赖项,测试时传入大量null不够优雅
  • 不清楚如何正确隔离依赖,聚焦Service核心逻辑测试

改进方案

1. 直接模拟IUnitOfWork接口(推荐)

Service依赖的是IUnitOfWork接口而非具体实现,完全可以直接模拟这个接口,跳过对真实UnitOfWork、Repository和DbContext的构造,让测试聚焦Service的业务逻辑。

这种方式的优势:

  • 无需关心UnitOfWork内部如何创建Repository
  • 彻底隔离底层数据访问层的实现细节
  • 测试代码更简洁、维护成本更低

示例代码:

[TestClass]
public class SomeUnitTest
{
    private SomeService _someService;
    private Mock<IUnitOfWork> _mockUnitOfWork;
    private Mock<ISomeRepository> _mockSomeRepository;

    [TestInitialize]
    public void Setup()
    {
        // 模拟IUnitOfWork和依赖的Repository
        _mockSomeRepository = new Mock<ISomeRepository>();
        _mockUnitOfWork = new Mock<IUnitOfWork>();
        _mockUnitOfWork.Setup(uow => uow.SomeRepository).Returns(_mockSomeRepository.Object);

        // 快速创建GenericService所需的其他依赖模拟(用Mock.Of<T>生成空实现)
        var mockMapper = Mock.Of<IMapper>();
        var mockValidateService = Mock.Of<IValidateService>();
        var mockSignalService = Mock.Of<ISignalService>();
        var mockLogBoekService = Mock.Of<ILogBoekService>();
        var mockHttpContextAccessor = Mock.Of<IHttpContextAccessor>();
        var mockUriService = Mock.Of<IUriService>();

        // 实例化Service
        _someService = new SomeService(
            _mockUnitOfWork.Object,
            mockMapper,
            mockValidateService,
            mockSignalService,
            mockLogBoekService,
            mockHttpContextAccessor,
            mockUriService
        );
    }

    [TestMethod]
    public async Task DoWork_WhenHasSomeItems_ReturnsFalse()
    {
        // 准备测试数据
        var someThings = new List<Some> {
            new Some { Id = 1, Name = "Some 1" },
            new Some { Id = 2, Name = "Some 2" }
        }.AsQueryable();

        // 设置Repository的All()方法返回预设数据
        _mockSomeRepository.Setup(repo => repo.All()).Returns(someThings);

        // 注意:原DoWork方法存在语法错误,IQueryable不能直接await,需改为CountAsync()
        // 修正后的DoWork:return await _unitOfWork.SomeRepository.All().CountAsync() == 0;
        var result = await _someService.DoWork();

        // 断言:当有数据时,DoWork应返回false
        Assert.IsFalse(result);
    }

    [TestMethod]
    public async Task DoWork_WhenNoItems_ReturnsTrue()
    {
        // 设置Repository返回空集合
        _mockSomeRepository.Setup(repo => repo.All()).Returns(Enumerable.Empty<Some>().AsQueryable());

        var result = await _someService.DoWork();

        // 断言:无数据时返回true
        Assert.IsTrue(result);
    }
}

2. 若需测试Repository与UnitOfWork的协作(可选)

如果需要验证UnitOfWork和Repository的交互逻辑(而非仅测试Service),可以优化DbContext的模拟方式,同时避免传入null依赖:

private IUnitOfWork CreateUnitOfWorkWithMockedDbContext(IQueryable<Some> someData)
{
    // 模拟DbSet
    var mockDbSet = new Mock<DbSet<Some>>();
    mockDbSet.As<IQueryable<Some>>()
        .Setup(m => m.Provider).Returns(someData.Provider)
        .Setup(m => m.Expression).Returns(someData.Expression)
        .Setup(m => m.ElementType).Returns(someData.ElementType)
        .Setup(m => m.GetEnumerator()).Returns(someData.GetEnumerator());

    // 模拟ApplicationDBContext
    var mockContext = new Mock<ApplicationDBContext>();
    mockContext.Setup(c => c.Some).Returns(mockDbSet.Object);

    // 提供合法的ILoggerFactory和IConfiguration模拟
    var mockLoggerFactory = Mock.Of<ILoggerFactory>();
    var mockConfiguration = Mock.Of<IConfiguration>();

    // 实例化真实的UnitOfWork
    return new UnitOfWork(mockContext.Object, mockLoggerFactory, mockConfiguration);
}

3. 封装常用模拟工具类

可以将DbSet模拟、依赖项模拟等逻辑封装成静态工具类,进一步简化测试代码:

public static class MockDbSetHelper
{
    public static Mock<DbSet<T>> CreateMockDbSet<T>(IQueryable<T> data) where T : class
    {
        var mockSet = new Mock<DbSet<T>>();
        mockSet.As<IQueryable<T>>()
            .Setup(m => m.Provider).Returns(data.Provider)
            .Setup(m => m.Expression).Returns(data.Expression)
            .Setup(m => m.ElementType).Returns(data.ElementType)
            .Setup(m => m.GetEnumerator()).Returns(data.GetEnumerator());
        return mockSet;
    }
}

关键注意事项

  • 接口优先:始终针对接口而非具体实现进行模拟,符合Clean架构的依赖倒置原则
  • 避免过度模拟:只模拟当前测试方法需要用到的依赖和方法,无关依赖用Mock.Of<T>()快速生成
  • 修正代码错误:原DoWork方法中await _unitOfWork.SomeRepository.All() ==0存在语法错误,All()返回IQueryable<T>不支持await,需改为await _unitOfWork.SomeRepository.All().CountAsync() == 0

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.30 22:24:21