如何在EF Core中Mock IExecutionStrategy.ExecuteAsync()方法
问题
项目中编写的业务代码里,公开方法CreateInstallmentDiscountAsync会调用私有方法。编写单元测试时遇到阻碍:在CreateDiscountAsync方法中,创建IExecutionStrategy对象后调用了扩展方法ExecuteAsync(this IExecutionStrategy strategy, Func<Task> operation),并传入一个委托。
目标是覆盖委托内的代码,验证调用await CompleteDiscountCreatingAsync(task, cancellationToken);后InstallmentPaymentTask task对象的状态变化。尝试MockIExecutionStrategy.ExecuteAsync方法,但无法让Mock方法执行代码中传入的委托,导致单元测试不符合预期。
业务代码
public async Task CreateInstallmentDiscountAsync ( InstallmentPaymentTask task, CancellationToken cancellationToken = default ) { try { await CreateDiscountAsync(task, cancellationToken); } catch (Exception ex) { task.SetDiscountCreatedError(ex.Message); _movedRepository.Update(task); await _movedRepository.SaveChangesAsync(cancellationToken); _loggingService.Log(LoggingLevel.Error, ex); } } private async Task<Order> CreateDiscountAsync ( InstallmentPaymentTask task, CancellationToken cancellationToken = default ) { var createDiscountCommand = BankInstallmentDiscountCommand.Create(task.OrderId, task.TotalAmount, task.AmountWithoutCommission); Order order = null; var strategy = _movedUnitOfWork.CreateExecutionStrategy(); await strategy.ExecuteAsync(async () => { await using var transaction = await _movedUnitOfWork.OpenTransactionAsync(cancellationToken); order = await _mediator.Send(createDiscountCommand, cancellationToken); await CompleteDiscountCreatingAsync(task, cancellationToken); await transaction.CommitAsync(cancellationToken); }); return order; } private async Task CompleteDiscountCreatingAsync ( InstallmentPaymentTask task, CancellationToken cancellationToken = default ) { task.CompleteDiscountCreating(); _movedRepository.Update(task); await _movedRepository.SaveChangesAsync(cancellationToken); }
错误的单元测试代码
[UnitTest] public async Task CreateInstallmentDiscount_Success_DiscountIsCreated() { var message = new InstallmentPaymentTaskMessage { CustomerEmail = "lol", OrderId = Guid.NewGuid(), PaymentDate = DateTime.UtcNow, AmountWithoutCommission = 9600, TotalAmount = 10000, }; var task = InstallmentPaymentTask.Create(message); var order = OrderFactory.CreateOrder(); var movedRepositoryMock = new Mock<IMovedRepository>(); movedRepositoryMock .Setup(_ => _.Update(task)); movedRepositoryMock .Setup(_ => _.SaveChangesAsync(It.IsAny<CancellationToken>())); var transactionMock = new Mock<IDbContextTransaction>(); transactionMock .Setup(_ => _.CommitAsync(It.IsAny<CancellationToken>())); var movedUnitOfWorkMock = new Mock<IMovedUnitOfWork>(); movedUnitOfWorkMock .Setup(_ => _.OpenTransactionAsync(It.IsAny<CancellationToken>())) .ReturnsAsync(transactionMock.Object); var executionStrategyMock = new Mock<IExecutionStrategy>(); executionStrategyMock .Setup(strategy => strategy.ExecuteAsync( It.IsAny<object>(), It.IsAny<Func<DbContext, object, CancellationToken, Task<object>>>(), It.IsAny<Func<DbContext, object, CancellationToken, Task<ExecutionResult<object>>>>(), It.IsAny<CancellationToken>() )) .ReturnsAsync(new ExecutionResult<object>(default, false)); movedUnitOfWorkMock .Setup(_ => _.CreateExecutionStrategy()) .Returns(executionStrategyMock.Object); var mediatorMock = new Mock<IMediator>(); mediatorMock .Setup(_ => _.Send(It.IsAny<BankInstallmentDiscountCommand>(), It.IsAny<CancellationToken>())) .ReturnsAsync(order); var rabbitMqServiceMock = new Mock<IRabbitMqService>(); rabbitMqServiceMock .Setup(_ => _.GetInstallmentPaymentTaskCreateQueue()) .Returns(new Queue { Name = "lol" }); rabbitMqServiceMock .Setup(_ => _.SendMessageAsync(It.IsAny<Queue>(), It.IsAny<string>())); var service = new InstallmentPaymentService(movedRepositoryMock.Object, mediatorMock.Object, movedUnitOfWorkMock.Object, rabbitMqServiceMock.Object, null); await service.CreateInstallmentDiscountAsync(task); task.Status.Should().Be(InstallmentPaymentTaskStatus.DiscountCreated); task.ErrorMessage.Should().BeNull(); rabbitMqServiceMock.Verify(_ => _.SendMessageAsync(It.IsAny<Queue>(), It.IsAny<string>()), Times.Never); movedUnitOfWorkMock.Verify(_ => _.OpenTransactionAsync(It.IsAny<CancellationToken>()), Times.Once); transactionMock.Verify(_ => _.CommitAsync(It.IsAny<CancellationToken>()), Times.Once); movedRepositoryMock.Verify(_ => _.SaveChangesAsync(It.IsAny<CancellationToken>()), Times.Once); }
解决方案
问题核心是MockIExecutionStrategy.ExecuteAsync时,既没有匹配业务代码实际调用的方法重载,也没有执行传入的委托。修复步骤如下:
1. 匹配正确的方法重载并执行委托
业务代码中调用的是接收Func<Task>的ExecuteAsync扩展方法,需要在Moq中匹配对应重载,并通过回调执行传入的委托。修改IExecutionStrategy的Mock代码:
var executionStrategyMock = new Mock<IExecutionStrategy>(); // 匹配接收Func<Task>的ExecuteAsync重载,直接执行传入的委托 executionStrategyMock .Setup(strategy => strategy.ExecuteAsync(It.IsAny<Func<Task>>(), It.IsAny<CancellationToken>())) .Returns((Func<Task> operation, CancellationToken token) => operation());
如果扩展方法实际调用的是带状态的重载,可调整为:
executionStrategyMock .Setup(strategy => strategy.ExecuteAsync( It.IsAny<object>(), It.IsAny<Func<DbContext, object, CancellationToken, Task>>(), It.IsAny<CancellationToken>() )) .Returns((object state, Func<DbContext, object, CancellationToken, Task> operation, CancellationToken token) => operation(null, state, token));
2. 修改后的完整单元测试
[UnitTest] public async Task CreateInstallmentDiscount_Success_DiscountIsCreated() { var message = new InstallmentPaymentTaskMessage { CustomerEmail = "lol", OrderId = Guid.NewGuid(), PaymentDate = DateTime.UtcNow, AmountWithoutCommission = 9600, TotalAmount = 10000, }; var task = InstallmentPaymentTask.Create(message); var order = OrderFactory.CreateOrder(); var movedRepositoryMock = new Mock<IMovedRepository>(); movedRepositoryMock .Setup(_ => _.Update(task)); movedRepositoryMock .Setup(_ => _.SaveChangesAsync(It.IsAny<CancellationToken>())) .ReturnsAsync(1); // 模拟保存成功 var transactionMock = new Mock<IDbContextTransaction>(); transactionMock .Setup(_ => _.CommitAsync(It.IsAny<CancellationToken>())) .Returns(Task.CompletedTask); var movedUnitOfWorkMock = new Mock<IMovedUnitOfWork>(); movedUnitOfWorkMock .Setup(_ => _.OpenTransactionAsync(It.IsAny<CancellationToken>())) .ReturnsAsync(transactionMock.Object); var executionStrategyMock = new Mock<IExecutionStrategy>(); // 关键修改:匹配正确重载并执行传入的委托 executionStrategyMock .Setup(strategy => strategy.ExecuteAsync(It.IsAny<Func<Task>>(), It.IsAny<CancellationToken>())) .Returns((Func<Task> operation, CancellationToken token) => operation()); movedUnitOfWorkMock .Setup(_ => _.CreateExecutionStrategy()) .Returns(executionStrategyMock.Object); var mediatorMock = new Mock<IMediator>(); mediatorMock .Setup(_ => _.Send(It.IsAny<BankInstallmentDiscountCommand>(), It.IsAny<CancellationToken>())) .ReturnsAsync(order); var rabbitMqServiceMock = new Mock<IRabbitMqService>(); rabbitMqServiceMock .Setup(_ => _.GetInstallmentPaymentTaskCreateQueue()) .Returns(new Queue { Name = "lol" }); rabbitMqServiceMock .Setup(_ => _.SendMessageAsync(It.IsAny<Queue>(), It.IsAny<string>())); var service = new InstallmentPaymentService(movedRepositoryMock.Object, mediatorMock.Object, movedUnitOfWorkMock.Object, rabbitMqServiceMock.Object, null); await service.CreateInstallmentDiscountAsync(task); // 验证状态变化 task.Status.Should().Be(InstallmentPaymentTaskStatus.DiscountCreated); task.ErrorMessage.Should().BeNull(); // 验证其他预期行为 rabbitMqServiceMock.Verify(_ => _.SendMessageAsync(It.IsAny<Queue>(), It.IsAny<string>()), Times.Never); movedUnitOfWorkMock.Verify(_ => _.OpenTransactionAsync(It.IsAny<CancellationToken>()), Times.Once); transactionMock.Verify(_ => _.CommitAsync(It.IsAny<CancellationToken>()), Times.Once); movedRepositoryMock.Verify(_ => _.SaveChangesAsync(It.IsAny<CancellationToken>()), Times.Once); }
说明
- 让Mock的
ExecuteAsync执行传入的委托,才能触发委托内的CompleteDiscountCreatingAsync方法,进而修改task的状态。 - 若不确定方法重载,可通过调试查看调用栈,或使用
It.IsAny<>匹配通用参数类型,捕获委托后执行。
内容的提问来源于stack exchange,提问作者Lesnykh Kirill
相关产品推荐
相关产品推荐

