如何在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
相关产品推荐
相关产品推荐

