如何正确Mock IAsyncRepository的GetByIdAsync方法完成单元测试?
问题:如何正确Mock
GetByIdAsync 方法完成单元测试? 问题背景
在站点验证逻辑中实现了Id存在性检查方法IdExistsAsync,该方法调用repository.GetByIdAsync判断模型是否存在;同时在ValidatorBase构造函数中针对Post请求设置了新Id不能重复的校验规则。基于继承ValidatorBase的DisciplineValidator编写单元测试时,MockIAsyncRepository<Discipline>的GetByIdAsync方法遇到两个问题:
- 最初的Mock写法未返回值,尝试添加
ReturnsAsync返回匹配的Discipline列表时,出现编译错误:“Isetup<IAsyncRepository, Task> does not contain a definition for 'ReturnsAsync'”。 - 修改为传入
ISpecification参数的Mock写法后,编译通过,但运行测试时抛出空引用异常:“System.ArgumentNullException : Value cannot be null. (Parameter 'source')”。
相关核心代码参考如下:
// Id存在性检查方法 private async Task<bool> IdExistsAsync(Guid id) { var model = await repository.GetByIdAsync(id); return model != null; } // ValidatorBase构造函数的校验规则 public ValidatorBase(IHttpContextAccessor httpContextAccessor, IAsyncRepository<T> repository) { this.repository = repository; if (httpContextAccessor.HttpContext.Request.Method == HttpMethods.Post) { RuleFor(model => model.Id).MustAsync(async (Id, cancellation) => !await IdExistsAsync(Id)) .WithMessage(string.Format("{0} Id already exists.", typeof(T).Name)); } } // 测试用Discipline集合 private static List<Discipline> testDisciplines = new List<Discipline>() { new Discipline { Id = new Guid("00000000-0000-0000-0000-000000000001"), Code = "001" }, new Discipline { Id = new Guid("00000000-0000-0000-0000-000000000002"), Code = "002" }, new Discipline { Id = new Guid("00000000-0000-0000-0000-000000000003"), Code = "003" } }; // 单元测试方法 public void UniqueIdCheckTest() { Discipline disciplineToTest = new Discipline() { Id = new Guid("00000000-0000-0000-0000-000000000001") }; var httpContextAccessorMock = new Mock<IHttpContextAccessor>(); httpContextAccessorMock.Setup(h => h.HttpContext.Request.Method).Returns(HttpMethods.Post); var disciplineRepositoryMock = new Mock<IAsyncRepository<Discipline>>(); // 修改后的Mock写法 disciplineRepositoryMock.Setup(t => t.GetByIdAsync(It.IsAny<ISpecification<Discipline>>(), default)) .ReturnsAsync(testDisciplines.Single(d => d.Id == disciplineToTest.Id)); var disciplineValidator = new DisciplineValidator(httpContextAccessorMock.Object, disciplineRepositoryMock.Object); var result = disciplineValidator.TestValidate(disciplineToTest); result.ShouldHaveValidationErrorFor(discipline => discipline.Id) .WithErrorMessage("Discipline id already exists."); }
问题原因分析
编译错误原因:
- 业务代码中
IdExistsAsync调用的是GetByIdAsync(Guid id)(单个Guid参数的重载),但你最初Mock时错误地返回了List<Discipline>,而GetByIdAsync的返回类型是Task<Discipline>,类型不匹配导致ReturnsAsync无法找到对应重载,触发编译错误。
- 业务代码中
空引用异常原因:
- Mock方法签名不匹配:修改后的Mock针对的是带
ISpecification参数的GetByIdAsync重载,但业务代码实际调用的是Guid参数版本,导致Mock未被命中,repository.GetByIdAsync(id)返回默认null; - HttpContext未正确Mock:仅Setup了
Request.Method,但未初始化HttpContext实例,访问httpContextAccessor.HttpContext.Request时抛出空引用异常。
- Mock方法签名不匹配:修改后的Mock针对的是带
正确解决方案
步骤1:修正HttpContextAccessor的Mock
需要完整MockHttpContext和HttpRequest的调用链,避免空引用:
var httpContextAccessorMock = new Mock<IHttpContextAccessor>(); var httpContext = new Mock<HttpContext>(); var request = new Mock<HttpRequest>(); request.Setup(r => r.Method).Returns(HttpMethods.Post); httpContext.Setup(h => h.Request).Returns(request.Object); httpContextAccessorMock.Setup(h => h.HttpContext).Returns(httpContext.Object);
步骤2:正确Mock Guid参数的GetByIdAsync重载
匹配业务代码实际调用的方法签名,返回对应Id的Discipline实例:
var disciplineRepositoryMock = new Mock<IAsyncRepository<Discipline>>(); disciplineRepositoryMock.Setup(repo => repo.GetByIdAsync(disciplineToTest.Id, default)) .ReturnsAsync(testDisciplines.Single(d => d.Id == disciplineToTest.Id));
若需匹配任意Guid参数,可替换为It.IsAny<Guid>(),但精确匹配目标Id更符合测试严谨性。
步骤3:将测试方法改为异步(规范做法)
由于校验规则使用了异步方法MustAsync,建议使用异步测试方法和异步验证接口,避免异步操作未完成就断言结果:
public async Task UniqueIdCheckTest() { Discipline disciplineToTest = new Discipline() { Id = new Guid("00000000-0000-0000-0000-000000000001") }; // 修正后的HttpContextAccessor Mock var httpContextAccessorMock = new Mock<IHttpContextAccessor>(); var httpContext = new Mock<HttpContext>(); var request = new Mock<HttpRequest>(); request.Setup(r => r.Method).Returns(HttpMethods.Post); httpContext.Setup(h => h.Request).Returns(request.Object); httpContextAccessorMock.Setup(h => h.HttpContext).Returns(httpContext.Object); // 正确Mock GetByIdAsync(Guid) var disciplineRepositoryMock = new Mock<IAsyncRepository<Discipline>>(); disciplineRepositoryMock.Setup(repo => repo.GetByIdAsync(disciplineToTest.Id, default)) .ReturnsAsync(testDisciplines.Single(d => d.Id == disciplineToTest.Id)); var disciplineValidator = new DisciplineValidator(httpContextAccessorMock.Object, disciplineRepositoryMock.Object); var result = await disciplineValidator.TestValidateAsync(disciplineToTest); result.ShouldHaveValidationErrorFor(discipline => discipline.Id) .WithErrorMessage("Discipline Id already exists."); }
额外说明
- 确认
IAsyncRepository<Discipline>存在GetByIdAsync(Guid id, CancellationToken cancellationToken = default)重载,这是业务代码实际调用的版本; - 使用
TestValidateAsync替代同步的TestValidate,更贴合异步校验的实际执行逻辑。
内容的提问来源于stack exchange,提问作者lross33
相关产品推荐
相关产品推荐

