-2

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.)

Chris Patterson
  • 28,659
  • 3
  • 47
  • 59
benjamin
  • 1,364
  • 1
  • 14
  • 26
  • You say you're using xunit, but you have an nunit annotation the test. Which is it? – possum Dec 24 '22 at 10:26
  • Oh good question! I must have been wrong about xUnit. I checked the dependencies. I think neither nUnit nor xUnit! I think it's actually MSTest. – benjamin Dec 24 '22 at 11:12
  • 1
    @Chris Patterson, the links you gave me have disappeared, but I think I know the ones you mean. I have written an alternative unit test method using the MassTransit test harness, thank you. And keep up the great work with MassTransit. – benjamin Dec 27 '22 at 13:23
  • Yeah, some other moderator deleted it. Glad you sorted it out! – Chris Patterson Dec 27 '22 at 19:56

2 Answers2

1

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.

Nkosi
  • 235,767
  • 35
  • 427
  • 472
  • Thanks, Nkosi! You must be right about this. It makes total sense, and I've implemented your suggestion. However, the test still fails. I wonder if the same thing is happening on another line? Maybe `CommandResponse.NotFound` also has to be a specific instance to pass? – benjamin Dec 26 '22 at 03:33
  • @benjamin where exactly does it fail if you step through the test – Nkosi Dec 26 '22 at 11:37
  • I've been around in circles, @Nkosi. Introduced new bugs and finally fixed them. I'm sorry. I have now got the test working and I couldn't have done it without you. I'm accepting your answer, thank you. – benjamin Dec 27 '22 at 13:14
1

I have accepted Nkosi's answer because it solved the problem stated in the question. Thank you, Nkosi.

However, the resources that were provided by the MassTransit boss-man, @Chris Patterson, were helpful in making a better unit test altogether. That's why I'm posting this alternative answer. This answer rewrites the original unit test to use the MassTransit test harness, and I hope it helps somebody one day.

Chris's links have somehow disappeared from sight. However, I think he posted this:

https://www.youtube.com/watch?v=Cx-Mc0DCpfE&t=545s

And this:

https://masstransit-project.com/usage/testing.html

The rewritten method:

public interface ICommand
{
    Guid OrderId { get; }
}

public record Command : ICommand
{
    public Command(Guid orderId)
    {
        OrderId = orderId;
    }

    public Guid OrderId { get; }
}

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;
        OrderId 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);
    }
}

The rewritten unit test class:

[TestClass()]
public class Handler_Tests
{
    private static OrderId ExistingOrderId = new OrderId("d94108e4-1121-401a-a6ef-c7736054041d");
    private static OrderId NonExistentOrderId = new OrderId("daaa72a0-8f8c-4a9b-b4ad-ebefbb6b5aa2");
    private static CustomerId ExistingCustomerId = new CustomerId("5fbf40d8-c064-4821-8948-a520863e6242");

    [TestMethod()]
    public async Task Consume_WithExistingOrderId_CancelsOrderAndSavesChangesAndReturnsSubmitted2()
    {
        // Arrange
        var harness = new InMemoryTestHarness();
        var mockRepository = GetMockOrderRepository();

        var sut = harness.Consumer(() =>
        {
            return new Handler(mockRepository.Object);
        });

        await harness.Start();

        try
        {
            var requestClient = await harness.ConnectRequestClient<ICommand>();
            var command = new Command(ExistingOrderId.Value);

            // Act
            var response = await requestClient.GetResponse<CommandResponse>(command, It.IsAny<CancellationToken>());

            // Assert
            mockRepository.Verify(x => x.SaveOrderAsync(It.IsAny<Order>(), It.IsAny<CancellationToken>()), Times.Once());
            response.Message.Result.Should().Be(CommandResponse.Results.Submitted);
        }
        finally
        {
            await harness.Stop();
        }
    }


    [TestMethod()]
    public async Task Consume_WithNonExistentOrderId_ReturnsNotFoundResponseAndDoesNotSave()
    {
        // Arrange
        var harness = new InMemoryTestHarness();
        var mockRepository = GetMockOrderRepository();

        var sut = harness.Consumer(() =>
        {
            return new Handler(mockRepository.Object);
        });

        await harness.Start();

        try
        {
            var requestClient = await harness.ConnectRequestClient<ICommand>();
            var command = new Command(NonExistentOrderId.Value);
            var response = await requestClient.GetResponse<CommandResponse>(command, It.IsAny<CancellationToken>());

            mockRepository.Verify(x => x.SaveOrderAsync(It.IsAny<Order>(), It.IsAny<CancellationToken>()), Times.Never());
            response.Message.Result.Should().Be(CommandResponse.Results.NotFound);
        }
        finally
        {
            await harness.Stop();
        }
    }


    private static Order GetTestOrder(OrderId orderId)
    {
        var orderItem = GetTestOrderItem();
        return Order.Place(orderId, ExistingCustomerId, new[] { orderItem });
    }

    private static OrderItem GetTestOrderItem()
    {
        var productId = new ProductId(Guid.NewGuid());
        var price = new Price(1, PurchaseCurrency.Usd);
        var quantity = new Quantity(1, UnitOfMeasure.Each);
        return new OrderItem(productId, quantity, price);
    }

    private static Mock<IOrderRepository> GetMockOrderRepository()
    {
        var mockRepository = new Mock<IOrderRepository>();

        mockRepository.Setup(x => x.FindOrderAsync(It.IsAny<OrderId>(), It.IsAny<CancellationToken>()))
        .ThrowsAsync(new InvalidOperationException(
            $"This mock is set up to work with {nameof(ExistingOrderId)} and {nameof(NonExistentOrderId)}."));

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

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

    
        return mockRepository;
    }
}
benjamin
  • 1,364
  • 1
  • 14
  • 26
  • My problem with these quasi integration tests are that they are slow. I just want to mock the ConsumeContext. – Rebecca Jan 10 '23 at 15:48