Introduction
In this post, we’ll look at MassTransit and MediatR integration testing. All the code for this can be found on my GitHub here.
If you’re interested in how to get MassTransit working with MediatR then take a look at my previous series:
- MassTransit and MediatR – Part 1 – Initial set up of MassTransit and MediatR
- MassTransit and MediatR – Part 2 – How to configure exponential back off when retrying messages
- MassTransit and MediatR – Part 3 – How to “dead letter” invalid messages with MassTransit, MediatR and FluentValidation
MassTransit Integration Tests
We’re going to continue with our Car Booking service we’ve been writing which does the below:
- Receive a
BookingCreated
message - Call the
CarBookingRepository
(which makes an API call to a 3rd party Car Booking API) - Send out a
CarBooked
message using MassTransit so that other downstream services can continue booking other things such as a hotel and flights.
We’ll split this out into two tests.
MassTransit and MediatR integration testing
Why are things a little more interesting with MediatR? To answer that, let’s remind ourselves what our BookingCreatedConsumer
looks like (see below). Note that IMediator
is injected using constructor injection:
using CarBooking.Application.Services.CarBookings.Commands.MakeCarBooking;
using Contracts.Messages;
using MassTransit;
using MediatR;
namespace CarBooking.Service.Consumers;
public class BookingCreatedConsumer : IConsumer<BookingCreated>
{
private readonly IMediator _mediator;
public BookingCreatedConsumer(IMediator mediator)
=> _mediator = mediator;
public async Task Consume(ConsumeContext<BookingCreated> context)
{
var bookingCreated = context.Message;
await MakeCarBookingAsync(bookingCreated);
await PublishCarBookedAsync(context, bookingCreated);
}
private async Task MakeCarBookingAsync(BookingCreated bookingCreated)
=> await _mediator.Send(new MakeCarBookingCommand(MapToCarBooking(bookingCreated)));
private static async Task PublishCarBookedAsync(ConsumeContext<BookingCreated> context, BookingCreated bookingCreated)
=> await context.Publish(MapToCarBooked(bookingCreated));
private static CarBooked MapToCarBooked(BookingCreated bookingCreated)
=> new(bookingCreated.BookingId, bookingCreated.BookingSummary,
bookingCreated.CarBooking, bookingCreated.HotelBooking, bookingCreated.FlightBooking);
private static Domain.Models.CarBooking MapToCarBooking(BookingCreated bookingCreated)
{
var carBooking = bookingCreated.CarBooking;
var bookingSummary = bookingCreated.BookingSummary;
return new(bookingCreated.BookingId, bookingSummary.FirstName, bookingSummary.LastName,
DateTime.UtcNow, DateTime.UtcNow, carBooking.PickUpLocation, 100,
Enum.Parse<Domain.Enums.Size>(carBooking.Size.ToString()),
Enum.Parse<Domain.Enums.Transmission>(carBooking.Transmission.ToString()));
}
}
We can just new up the BookingCreatedConsumer
with a mock of IMediator
and pass a mock of the ConsumeContext
to the Consume()
method then assert that various methods are called on the two mocks but this isn’t really an integration test as it’s just testing one class and mocking out all dependencies. What we ideally want to do is spin up the service in memory, send a message and assert that we attempt to call the 3rd party Car API via a mock of the CarBookingRepository
. To do this, we’ll useWebApplicationFactory
and MassTransit Test Harness. See the next steps.
1 – Create WebApplicationFactory instance with MassTransit Test Harness
This first test will assert that:
- We consume the
BookingCreated
message - We call a mock of the
CarBookingRepository
(which is the service which calls our 3rd party Car Booking API)
Creating a new WebApplicationFactory
for each test is quite slow as it takes 400ms or so which means that’s going to add up if you have many tests. In this case, we’ll create a ServiceTestsContext
class which we can inject into our test class and use the same instance of the WebApplicationFactory
for all tests. See below and note line 21-23 where we’re adding the MassTransit test harness and registering our mock ICarBookingRepository
.
using CarBooking.Application.Repositories;
using CarBooking.Service.Consumers;
using MassTransit;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.DependencyInjection;
using Moq;
namespace CarBooking.Service.Tests.Component;
public class ServiceTestsContext
{
public Mock<ICarBookingRepository> MockCarBookingRepository { get; private set; }
public ServiceTestsContext() => MockCarBookingRepository = new Mock<ICarBookingRepository>();
public WebApplicationFactory<Startup> WebApplicationFactory
=> new WebApplicationFactory<Startup>()
.WithWebHostBuilder(b => b.ConfigureServices(services
=> RegisterServices((ServiceCollection)services)));
private void RegisterServices(ServiceCollection services)
=> services.AddMassTransitTestHarness(cfg => cfg.AddConsumer<BookingCreatedConsumer>())
.AddSingleton(MockCarBookingRepository.Object);
}
2 – Create new test class and inject ServiceTestsContext using xUnit IClassFixture
To do this, we simply create a new CarBookingServiceTests
class which implements IClassFixture<ServiceTestsContext>
and use constructor injection to inject our ServiceTestsContext
. This is the Class Fixture feature within xUnit and you can find out more about that here. Basically CarBookingServiceTests
will use the same ServiceTestsContext
for all tests in the class so it saves the time of newing up the WebApplicationFactory
each time.
using CarBooking.Service.Consumers;
using Contracts.Messages;
using Contracts.Messages.Enums;
using MassTransit;
using MassTransit.Testing;
using Microsoft.Extensions.DependencyInjection;
using Moq;
using Xunit;
namespace CarBooking.Service.Tests.Component;
public class CarBookingServiceTests : IClassFixture<ServiceTestsContext>
{
private readonly ServiceTestsContext _context;
public CarBookingServiceTests(ServiceTestsContext context) => _context = context;
}
3 – Set up mock CarBookingRepository
Let’s start writing our test and just create a BookingCreated
message and set up our mock which has a call back so we can assert properties on actualCarBooking
:
[Fact]
public async void GivenNewBookingCreatedEvent_WhenThisIsReceived_ThenMakesACarBooking()
{
var bookingCreated = BuildBookingCreated();
Domain.Models.CarBooking? actualCarBooking = null;
_context.MockCarBookingRepository.Setup(m => m.SendAsync(It.Is<Domain.Models.CarBooking>(c
=> c.BookingId == bookingCreated.BookingId.ToString())))
.Callback<Domain.Models.CarBooking>(c => actualCarBooking = c);
}
Conclusion
In this post, we’ve created a MassTransit test harness, linked it up to a WebApplicationFactory
and have started writing out our MassTransit integration test. In the next part, we’ll look at how we start the MassTransit test harness and use it to send a message and assert it was consumed.
Happy testing!