.NET 6 Web API Moq单元测试如何绕过服务内部SelectFoo2方法
问题背景
我在.NET 6中编写Web API的服务层代码,实现如下:
public async Task<Foo1> GetFileByObjectId(int objectId) { List<Foo2> selectedFoo2 = (await SelectFoo2(objectId)).ToList(); if (selectedFoo2.Count == 0) { throw new HttpStatusException(HttpStatusCode.InternalServerError, 22, $"The object doesn't exist."); } if (!IsAdmin) { List<Permission> permissions = (await _permissionService.Select(username)).ToList(); if (permissions.Count == 0 || permissions.Count() == 0) { throw new HttpStatusException(HttpStatusCode.InternalServerError, 23, $"The User {username} doesn't own the necessary rights"); } } // 剩余业务逻辑省略 }
目前使用Moq编写单元测试,用于验证权限缺失的异常场景,测试代码如下:
public async Task TestGetFileByObjectId() { // Arrange Mock.Get(_permissionService).Setup(mock => mock.Select(It.IsAny<int>(), It.IsAny<string>(), string.Empty)).Returns(Task.FromResult(Enumerable.Repeat(Mock.Of<Permission>(), 0))); var configValues = new Dictionary<string, string> { {"AppSettings:RootFolder", "..."}, }; var configuration = new ConfigurationBuilder() .AddInMemoryCollection(configValues) .Build(); Foo2Service myService = new Foo2Service(..., _permissionService, ...); // Act // Assert HttpStatusException exceptionThrown = await Assert.ThrowsExceptionAsync<HttpStatusException>(async () => await myService.GetFileByObjectId(It.IsAny<int>())); Assert.IsTrue(exceptionThrown.CustomErrorCode == 23); }
现存问题
SelectFoo2是Foo2Service自身的方法,并非外部注入的服务,执行时会请求数据库连接字符串,我不需要测试该方法的SQL查询执行逻辑,如何在单元测试中绕过SelectFoo2方法?
补充代码说明
SelectFoo2方法(不希望测试其中的查询执行逻辑)实现如下:
public async Task<IEnumerable<Foo2>> SelectFoo2(int objectId) { // Query sql using RepoDB return await ExecuteQueryAsync<EcmObject>(sql, param); }
基类BaseService中的ExecuteQueryAsync方法实现如下:
public async Task<IEnumerable<T>> ExecuteQueryAsync<T>(string sql, Dictionary<string, object> param) { using (SqlConnection connection = new SqlConnection(_ecmCommonInfoService.ConnectionString)) { connection.Open(); IEnumerable<T> records = await connection.ExecuteQueryAsync<T>(sql, param); return records; } }
解决方案
有两种常用实现方式,按需选择即可:
方案1(推荐,符合设计规范):抽离数据访问依赖
当前代码将业务逻辑和数据访问逻辑耦合在同一个服务类中,本身不符合单一职责原则。可以将SelectFoo2对应的数据库查询逻辑抽离到独立的仓储类中,通过接口注入:
- 定义仓储接口
public interface IFoo2Repository { Task<IEnumerable<Foo2>> SelectFoo2(int objectId); }
- 将原有
SelectFoo2的SQL查询逻辑移动到IFoo2Repository的实现类中,在Foo2Service的构造函数中注入该依赖,将原代码中await SelectFoo2(objectId)的调用替换为await _foo2Repository.SelectFoo2(objectId)。 - 单元测试时直接Mock
IFoo2Repository,配置返回非空的Foo2列表即可跳过数据库访问逻辑,直接进入后续权限校验流程:
var mockFoo2Repo = new Mock<IFoo2Repository>(); // 配置返回测试用的非空数据,跳过对象不存在的校验分支 mockFoo2Repo.Setup(r => r.SelectFoo2(It.IsAny<int>())) .ReturnsAsync(new List<Foo2> { new Foo2() }); // 注入Mock的仓储实例创建服务 var myService = new Foo2Service(mockFoo2Repo.Object, _permissionService, /* 其他依赖 */);
这种方式实现的测试逻辑最干净,也能优化生产代码的结构,是首选方案。
方案2(无需重构生产代码):使用Moq部分模拟
如果暂时不想调整生产代码结构,可以用Moq的部分模拟功能拦截SelectFoo2的执行,注意需要先将SelectFoo2方法标记为virtual,Moq才能重写该方法的返回值:
- 修改服务类中
SelectFoo2的定义,加virtual关键字:
public virtual async Task<IEnumerable<Foo2>> SelectFoo2(int objectId) { // 原有逻辑不变 }
- 单元测试中不要直接
new服务实例,通过Mock<Foo2Service>创建实例,配置SelectFoo2的返回值:
// 创建服务Mock,CallBase设为true表示未Setup的方法走原有逻辑 var serviceMock = new Mock<Foo2Service>(/* 传入构造函数所需的所有依赖实例 */) { CallBase = true }; // 拦截SelectFoo2,直接返回测试数据,不执行原方法的数据库访问 serviceMock.Setup(s => s.SelectFoo2(It.IsAny<int>())) .ReturnsAsync(new List<Foo2> { new Foo2() }); var myService = serviceMock.Object;
原有测试代码的注意点
你当前写的测试代码有两个细节问题会导致测试不通过:
_permissionService.Select的Setup参数和实际调用不匹配:业务代码中调用的是_permissionService.Select(username),只有1个参数,但你Setup时写了3个参数,Moq无法匹配到这个配置,不会返回你设置的空权限列表。- 调用被测方法时不能传
It.IsAny<int>():It.IsAny<T>()只能用于Mock配置时的参数匹配,实际执行方法要传入具体的测试值,比如1。
内容的提问来源于stack exchange,提问作者Emanuele
相关产品推荐
相关产品推荐

