Moq Verify 表示方法从未被调用,但实际上它已经被调用了。

huangapple go评论58阅读模式
英文:

Moq Verify says method was never called, but it was

问题

以下是代码部分的中文翻译:

public class KafkaConsumerService : BackgroundService
{
    private readonly IConsumer<string, Transaction> _kafkaConsumer;
    private readonly IRepository _repository;
    private readonly ICalculator _calculator;

    public KafkaConsumerService(
        IConsumer<string, Transaction> kafkaConsumer,
        IRepository repository,
        ICalculator calculator
    )
    {
        _kafkaConsumer = kafkaConsumer;
        _repository = repository;
        _calculator = calculator;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        var consumeResult = await Task.Run(() => _kafkaConsumer.Consume(stoppingToken), stoppingToken);
        var transaction = consumeResult.Message.Value;

        var account = await _repository.GetAccount(transaction.Account);
        await _repository.UpdateAccount(_calculator.CalculateAccount(account, Normalize(transaction)));
    }

    private Transaction Normalize(Transaction transaction)
    {
        if (!transaction.IsCancellation)
        {
            return transaction;
        }

        return new Transaction(transaction)
        {
            Amount = transaction.Amount * -1,
            IsCancellation = false
        };
    }
}
public class KafkaConsumerServiceTest
{
    private readonly Mock<IConsumer<string, Transaction>> _kafka = new();
    private readonly Mock<IRepository> _repository = new();
    private readonly Mock<ICalculator> _calculator = new();

    private readonly Fixture _fixture = new();
    private readonly KafkaConsumerService _kafkaConsumerService;

    public KafkaConsumerServiceTest()
    {
        _kafkaConsumerService = new KafkaConsumerService(_kafka.Object, _repository.Object, _calculator.Object);
    }

    [Fact]
    public async Task KafkaConsumerService_ProcessesCancelationTransaction()
    {
        _fixture.Customize<Transaction>(composer => composer
            .With(transaction => transaction.IsCancellation, true)
        );

        var transaction = _fixture.Create<Transaction>();
        _kafka
            .Setup(consumer => consumer.Consume(It.IsAny<CancellationToken>()))
            .Returns(new ConsumeResult<string, Transaction>
            {
                Message = new Message<string, Transaction>
                {
                    Value = transaction,
                },
            });

        var result = _fixture.Create<Account>() with
        {
            AccountName = transaction.Account
        };

        _repository
            .Setup(repository => repository.GetAccount(transaction.Account))
            .ReturnsAsync(result);

        _calculator
            .Setup(calculator => calculator.CalculateAccount(It.IsAny<Account?>(), It.IsAny<Transaction>()))
            .Returns(result);

        await _kafkaConsumerService.StartAsync(CancellationToken.None);

        _repository.Verify(repository =>
            repository.GetAccount(transaction.Account)
        );
        _calculator.Verify(calculator =>
            calculator.CalculateAccount(result, transaction)
        );
        _repository.Verify(repository => repository.UpdateAccount(result));
    }
}

这些是您提供的代码的中文翻译部分。如果您需要更多帮助或其他问题的翻译,请随时告诉我。

英文:

I have the following code which reads a Transaction from Kafka, and updates the account balance to show that transaction

public class KafkaConsumerService : BackgroundService
{
    private readonly IConsumer&lt;string, Transaction&gt; _kafkaConsumer;
    private readonly IRepository _repository;
    private readonly ICalculator _calculator;

    public KafkaConsumerService(
        IConsumer&lt;string, Transaction&gt; kafkaConsumer,
        IRepository repository,
        ICalculator calculator
    )
    {
        _kafkaConsumer = kafkaConsumer;
        _repository = repository;
        _calculator = calculator;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        var consumeResult = await Task.Run(() =&gt; _kafkaConsumer.Consume(stoppingToken), stoppingToken);
        var transaction = consumeResult.Message.Value;

        var account = await _repository.GetAccount(transaction.Account);
        await _repository.UpdateAccount(_calculator.CalculateAccount(account, Normalize(transaction)));
    }

    private Transaction Normalize(Transaction transaction)
    {
        if (!transaction.IsCancellation)
        {
            return transaction;
        }

        return new Transaction(transaction)
        {
            Amount = transaction.Amount * -1,
            IsCancellation = false
        };
    }
}

I have then written the following unit test for this, using XUnit and Moq

public class KafkaConsumerServiceTest
{
    private readonly Mock&lt;IConsumer&lt;string, Transaction&gt;&gt; _kafka = new();
    private readonly Mock&lt;IRepository&gt; _repository = new();
    private readonly Mock&lt;ICalculator&gt; _calculator = new();

    private readonly Fixture _fixture = new();
    private readonly KafkaConsumerService _kafkaConsumerService;

    public KafkaConsumerServiceTest()
    {
        _kafkaConsumerService = new KafkaConsumerService(_kafka.Object, _repository.Object, _calculator.Object);
    }

    [Fact]
    public async Task KafkaConsumerService_ProcessesCancelationTransaction()
    {
        _fixture.Customize&lt;Transaction&gt;(composer =&gt; composer
            .With(transaction =&gt; transaction.IsCancellation, true)
        );

        var transaction = _fixture.Create&lt;Transaction&gt;();
        _kafka
            .Setup(consumer =&gt; consumer.Consume(It.IsAny&lt;CancellationToken&gt;()))
            .Returns(new ConsumeResult&lt;string, Transaction&gt;
            {
                Message = new Message&lt;string, Transaction&gt;
                {
                    Value = transaction,
                },
            });

        var result = _fixture.Create&lt;Account&gt;() with
        {
            AccountName = transaction.Account
        };

        _repository
            .Setup(repository =&gt; repository.GetAccount(transaction.Account))
            .ReturnsAsync(result);

        _calculator
            .Setup(calculator =&gt; calculator.CalculateAccount(It.IsAny&lt;Account?&gt;(), It.IsAny&lt;Transaction&gt;()))
            .Returns(result);

        await _kafkaConsumerService.StartAsync(CancellationToken.None);

        _repository.Verify(repository =&gt;
            repository.GetAccount(transaction.Account)
        );
        _calculator.Verify(calculator =&gt;
            calculator.CalculateAccount(result, transaction)
        );
        _repository.Verify(repository =&gt; repository.UpdateAccount(result));
    }
}

However I then get the following error

Moq.MockException

Expected invocation on the mock at least once, but was never performed: repository =&gt; repository.GetAccount(&quot;Account73ccea18-e39c-493f-9533-7af7f983b8ab&quot;)

Performed invocations:

   Mock&lt;IRepository:1&gt; (repository):

      IRepository.GetAccount(&quot;Account73ccea18-e39c-493f-9533-7af7f983b8ab&quot;)
      IRepository.UpdateAccount(Account { AccountName = Account73ccea18-e39c-493f-9533-7af7f983b8ab, Amount = 119 })

As you can see it says the method GetAccount(&quot;Account73ccea18-e39c-493f-9533-7af7f983b8ab&quot;) was never called, however right below it under Performed invocations, it says it was called.

If anyone has any ideas as to what is going wrong here I would appreciate it.

EDIT
Adding an await Task.Delay(100) on the unit tests seem to fix the problem, however this isnt an ideal solution, and I still dont understand why the issue occurs in the first place.

EDIT #2
It seems that removing the extension of BackgroundService (https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.hosting.backgroundservice?view=dotnet-plat-ext-7.0) seems to fix the test aswell. Could this somehow be causing a race condition in my code?

答案1

得分: 0

I think the culprit could be this:

return new Transaction(transaction)
{
    Amount = transaction.Amount * -1,
    IsCancellation = false
};

When you verify against an instance, it is doing a reference check so it cannot be a different newly made object.

Try

_repository.Verify(repository =>
    repository.GetAccount(It.IsAny<string>())
);
_repository.Verify(repository => repository.UpdateAccount(It.IsAny<Transaction>()));

You may also use It.Is<Transaction>(t => t.AccountName == "account") to validate specific values in the assertion.

英文:

I think the culprit could be this:

return new Transaction(transaction)
{
    Amount = transaction.Amount * -1,
    IsCancellation = false
};

When you verify against an instance, it is doing a reference check so it cannot be a different newly made object.

Try

_repository.Verify(repository =&gt;
    repository.GetAccount(It.IsAny&lt;string&gt;())
);
_repository.Verify(repository =&gt; repository.UpdateAccount(It.IsAny&lt;Transaction&gt;()));

You may also use It.Is&lt;Transaction&gt;(t =&gt; t.AccountName == &quot;account&quot;) to validate specific values in the assertion.

答案2

得分: 0

以下是您要翻译的代码部分:

查看更多细节。看起来 `BackgroundService.StartAsync` 将调用 `ExecuteAsync`,然后返回 `Task.CompletedTask`。

public virtual Task StartAsync(CancellationToken cancellationToken)
{
     // 存储我们正在执行的任务
    _executingTask = ExecuteAsync(_stoppingCts.Token);

    // 如果任务已经完成,则返回它,
    // 这将传递取消和失败给调用者
    if (_executingTask.IsCompleted)
    {
        return _executingTask;
    }

    // 否则,它正在运行
    return Task.CompletedTask;
}

这导致我的代码尚未完成执行,因此我的 moq 断言失败。我认为这两者之间的时间非常接近,因此在生成错误时,方法已被调用,这就是令人困惑的错误消息原因。

我通过简单地等待执行的任务完成来解决了这个问题:

await _kafkaConsumerService.StartAsync(CancellationToken.None);
await _kafkaConsumerService.ExecuteTask;

(请注意,上述代码是根据您提供的信息翻译的,其中包含了代码部分。)

英文:

Looking into this further. It appears that BackgroundService.StartAsync will call ExecuteAsync, and then return Task.CompletedTask

public virtual Task StartAsync(CancellationToken cancellationToken)
{
     // Store the task we&#39;re executing
    _executingTask = ExecuteAsync(_stoppingCts.Token);

    // If the task is completed then return it,
    // this will bubble cancellation and failure to the caller
    if (_executingTask.IsCompleted)
    {
        return _executingTask;
    }

    // Otherwise it&#39;s running
    return Task.CompletedTask;
}

This then meant that my code has not finished executing yet, hence why my moq assertions failed. I then assume that the timing between these two was very close, so that by the time the error was generated, the methods had been called hence the confusing error message.

I fixed this issue by simply waiting for the executed task to be completed

await _kafkaConsumerService.StartAsync(CancellationToken.None);
await _kafkaConsumerService.ExecuteTask;

huangapple
  • 本文由 发表于 2023年2月19日 00:07:17
  • 转载请务必保留本文链接:https://go.coder-hub.com/75494603.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定