You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.14 02:12:04