ASP.NET Core 6中Command Handler单元测试问题(无需仓储模式)
问题:ASP.NET Core 6模拟DbContext时抛出NotSupportedException
在为CreateQueryCommandHandler编写单元测试时,使用Moq模拟QueryStoreDbContext的Add方法时触发System.NotSupportedException,错误提示不可重写的成员(QueryStoreDbContext.get_Queries)不能用于设置/验证表达式,要求在不使用仓储模式的前提下解决该问题。
错误信息
System.NotSupportedException: Unsupported expression: x => x.Queries Non-overridable members (here: QueryStoreDbContext.get_Queries) may not be used in setup / verification expressions. at Moq.Guard.IsOverridable(MethodInfo method, Expression expression) in /_/src/Moq/Guard.cs:line 99 at Moq.ExpressionExtensions.<Split>g__Split|5_0(Expression e, Expression& r, MethodExpectation& p, Boolean assignment, Boolean allowNonOverridableLastProperty) in /_/src/Moq/ExpressionExtensions.cs:line 191 at Moq.ExpressionExtensions.Split(LambdaExpression expression, Boolean allowNonOverridableLastProperty) in /_/src/Moq/ExpressionExtensions.cs:line 170 at Moq.Mock.SetupRecursive[TSetup](Mock mock, LambdaExpression expression, Func`4 setupLast, Boolean allowNonOverridableLastProperty) in /_/src/Moq/Mock.cs:line 728 at Moq.Mock.Setup(Mock mock, LambdaExpression expression, Condition condition) in /_/src/Moq/Mock.cs:line 562 at Moq.Mock`1.Setup[TResult](Expression`1 expression) in /_/src/Moq/Mock`1.cs:line 645 at QueryStore.Application.UnitTests.Queries.Commands.CreateQueryCommandHandlerTests.Handle_Should_ReturnSuccessResult_WhenQueryIsAddedToDatabase() in Application/tests/Queries/Commands/CreateQueryCommandHandlerTests.cs:line 96
原测试代码(问题片段)
[Fact] public async Task Handle_Should_ReturnSuccessResult_WhenQueryIsAddedToDatabase() { var command = new CreateQueryCommand() { Name = "Some Name", Content = "Select * from AbcCompany", Description = "Some description about the query.", Tags = new[] { "company", "import" } }; // 这里触发错误:尝试模拟非虚的Queries属性 _contextMock.Setup(x => x.Queries.Add(It.IsAny<Query>())); var handler = new CreateQueryCommandHandler(_contextMock.Object, _validator, _loggerMock.Object); var validationResult = await _validator.ValidateAsync(command); var result = await handler.Handle(command, CancellationToken.None); _contextMock.Verify(x => x.Queries.Add(It.IsAny<Query>()), Times.Once); _contextMock.Verify(x => x.SaveChangesAsync(CancellationToken.None), Times.Once); Assert.True(validationResult.IsValid); Assert.IsType<Result<int>>(result); Assert.True(result.IsSuccess); Assert.True(result.Value > 0); }
处理器核心代码
try { var entity = request.AsQuery(); // 实际调用的是DbContext的Add方法,而非DbSet的Add _context.Add(entity); await _context.SaveChangesAsync(cancellationToken); return Result<int>.Success(entity.Id); }
解决方案
核心思路
EF Core自动生成的DbSet属性默认是非虚(non-virtual)的,Moq无法模拟这类成员。但观察处理器代码,实际调用的是DbContext基类的Add<TEntity>方法而非Queries.Add,因此直接模拟DbContext的Add和SaveChangesAsync方法即可,无需触碰Queries属性。
修改后的测试代码
[Fact] public async Task Handle_Should_ReturnSuccessResult_WhenQueryIsAddedToDatabase() { var command = new CreateQueryCommand() { Name = "Some Name", Content = "Select * from AbcCompany", Description = "Some description about the query.", Tags = new[] { "company", "import" } }; // 捕获添加的Query实体,用于后续设置Id Query addedQuery = null; _contextMock.Setup(x => x.Add(It.IsAny<Query>())) .Callback<Query>(query => addedQuery = query); // 模拟SaveChangesAsync返回成功,并设置实体Id为预期值 _contextMock.Setup(x => x.SaveChangesAsync(It.IsAny<CancellationToken>())) .ReturnsAsync(1) .Callback(() => addedQuery.Id = 1); var handler = new CreateQueryCommandHandler(_contextMock.Object, _validator, _loggerMock.Object); var validationResult = await _validator.ValidateAsync(command); var result = await handler.Handle(command, CancellationToken.None); // 验证方法调用次数 _contextMock.Verify(x => x.Add(It.IsAny<Query>()), Times.Once); _contextMock.Verify(x => x.SaveChangesAsync(CancellationToken.None), Times.Once); // 断言结果 Assert.True(validationResult.IsValid); Assert.IsType<Result<int>>(result); Assert.True(result.IsSuccess); Assert.Equal(1, result.Value); }
可选优化(针对后续扩展)
如果后续测试需要模拟DbSet的查询操作,可以将DbContext中的DbSet属性改为虚(virtual):
public class QueryStoreDbContext : DbContext { // 将DbSet标记为virtual,允许Moq模拟 public virtual DbSet<Query> Queries { get; set; } // ...其他上下文配置代码 }
但针对当前处理器的逻辑,此优化非必需。
内容的提问来源于stack exchange,提问作者Hardik Dhuri
相关产品推荐
相关产品推荐

