Search code examples
c#moqmstestfluent-assertions

C# unit testing MassTransit handler with MSTest, Moq and FluentAssertions. Can't verify method called exactly once


I have this class called Handler, which is a MassTransit IConsumer:

public class Handler : IConsumer<ICommand>
{
    private readonly IOrderRepository _orderRepository;

    public Handler(IOrderRepository orderRepository)
    {
        _orderRepository = orderRepository ?? throw new ArgumentNullException(nameof(orderRepository));
    }


    public async Task Consume(ConsumeContext<ICommand> context)
    {
        var command = context.Message;
        var orderId = new OrderId(command.OrderId);
        var order = await _orderRepository.FindOrderAsync(orderId, context.CancellationToken);

        if (order is null)
        {
            await context.RespondAsync(CommandResponse.NotFound);
            return;
        }

        order.Cancel();
        await _orderRepository.SaveOrderAsync(order, context.CancellationToken);
        await context.RespondAsync(CommandResponse.Submitted);
    }
}

I have two unit tests for it. Here's the one that seems to work fine:

    [TestMethod]
    public async Task Consume_WithExistingOrderId_CancelsOrderAndSavesChangesAndReturnsSubmitted()
    {
        // Arrange
        var mockConsumer = new Mock<IConsumer<ICommand>>();
        var mockRepository = new Mock<IOrderRepository>();
        var sut = new Handler(mockRepository.Object);

        var mockCommand = new Mock<ICommand>();
        var mockContext = new Mock<ConsumeContext<ICommand>>();
        mockContext.Setup(x => x.Message).Returns(mockCommand.Object);
        mockContext.Setup(x => x.RespondAsync(It.IsAny<CommandResponse>())).Returns(Task.CompletedTask);

        var existingOrderId = new OrderId(Guid.NewGuid());
        mockCommand.Setup(x => x.OrderId).Returns(existingOrderId.Value);

        var order = GetTestOrder(existingOrderId);
        mockRepository.Setup(x => x.FindOrderAsync(existingOrderId, It.IsAny<CancellationToken>())).ReturnsAsync(order);

        // Act
        await sut.Consume(mockContext.Object);

        // Assert
        mockRepository.Verify(x => x.SaveOrderAsync(order, It.IsAny<CancellationToken>()), Times.Once());
        mockContext.Verify(x => x.RespondAsync(CommandResponse.Submitted), Times.Once());
        order.IsCancelled.Should().BeTrue();
    }

And here's the one that isn't doing what I expected:

 [TestMethod()]
    public async Task Consume_WithNonExistantOrderId_ReturnsNotFoundResponseAndDoesNotSave()
    {
        // Arrange
        var mockRepository = new Mock<IOrderRepository>();
        var sut = new Handler(mockRepository.Object);

        var mockCommand = new Mock<ICommand>();
        var mockContext = new Mock<ConsumeContext<ICommand>>();
        mockContext.Setup(x => x.Message).Returns(mockCommand.Object);
        mockContext.Setup(x => x.RespondAsync(It.IsAny<CommandResponse>())).Returns(Task.CompletedTask);

        var nonExistantOrderId = new OrderId(Guid.NewGuid());
        mockCommand.Setup(x => x.OrderId).Returns(nonExistantOrderId.Value);

        mockRepository.Setup(x => x.FindOrderAsync(nonExistantOrderId, It.IsAny<CancellationToken>())).ReturnsAsync((Order?)null);
        // Act
        await sut.Consume(mockContext.Object);

        // Assert
        mockRepository.Verify(x => x.SaveOrderAsync(It.IsAny<Order>(), It.IsAny<CancellationToken>()), Times.Never());
        mockContext.Verify(x => x.RespondAsync(CommandResponse.NotFound), Times.Once());
    }

Both unit tests require that the Handler calls the RespondAsync method of the MassTransit context exactly once. However, the second unit test doesn't pass, saying that the method was never called. I don't see why it was never called. When I debug into the method it appears to show the method is called.

I can't tell if my test is wrong or if my system under test is wrong. Can anybody see the problem please?

(Also, if anybody can see how to make my code more testable and my unit tests shorter and simpler that would also be appreciated.)


Solution

  • The problem is with the nonExistantOrderId and using that for the match in expectation.

    mockRepository
        .Setup(x => x.FindOrderAsync(nonExistantOrderId, It.IsAny<CancellationToken>()))
        .ReturnsAsync((Order?)null);
    

    the mock expects to get that specific instance when the subject is being exercised but the subject initialized its own instance which causes the mock to not invoke the async call and exit the subject before that target line can be invoked.

    This is why

    mockRepository.Verify(x => x.SaveOrderAsync(It.IsAny<Order>(), It.IsAny<CancellationToken>()), Times.Never());
    

    supposedly passed verification, and

    mockContext.Verify(x => x.RespondAsync(CommandResponse.NotFound), Times.Once());
    

    failed since the subject code exited before reaching both members that are the targets of your verification.

    Loosen the match using It.IsAny<OrderId>()

    mockRepository
        .Setup(x => x.FindOrderAsync(It.IsAny<OrderId>(), It.IsAny<CancellationToken>()))
        .ReturnsAsync((Order?)null);
    

    so that the mocked async call can be invoked and allow the code to flow to completion.