Home > front end >  How to inject MassTransit's IPublishEndpoint when using InMemoryTestHarness
How to inject MassTransit's IPublishEndpoint when using InMemoryTestHarness

Time:10-05

Overview

So I have an existing web api that uses MediatR commands. I am working on integrating MT into some of them to publish messages to RMQ for various purposes. I am able to integrate it into my commands just fine, but I'm running into issues when testing with the InMemoryHarness during my integration tests.

Details

I have an NUnit Test Fixture that sets up my in memory harness like so, just like the docs describe.

[OneTimeSetUp]
public async Task RunBeforeAnyTests()
{
    var dockerDbPort = await DockerDatabaseUtilities.EnsureDockerStartedAndGetPortPortAsync();
    var dockerConnectionString = DockerDatabaseUtilities.GetSqlConnectionString(dockerDbPort.ToString());

    var builder = new ConfigurationBuilder()
        .SetBasePath(Directory.GetCurrentDirectory())
        .AddEnvironmentVariables();

    _configuration = builder.Build();

    var services = new ServiceCollection();

    // various other registration

    // MassTransit Setup -- Do Not Delete Comment
    _provider = services.AddMassTransitInMemoryTestHarness(cfg =>
    {
        // Consumers here
    }).BuildServiceProvider(true);
    _harness = _provider.GetRequiredService<InMemoryTestHarness>();
            
    await _harness.Start();
}

I also have a MediatR handlers that might look something like this:

public class Handler : IRequestHandler<AddRecipeCommand, RecipeDto>
{
    private readonly RecipesDbContext _db;
    private readonly IMapper _mapper;
    private readonly IPublishEndpoint _publishEndpoint;

    public Handler(RecipesDbContext db, IMapper mapper, IPublishEndpoint publishEndpoint)
    {
        _mapper = mapper;
        _publishEndpoint = publishEndpoint;
        _db = db;
    }

    public async Task<RecipeDto> Handle(AddRecipeCommand request, CancellationToken cancellationToken)
    {
        var recipe = _mapper.Map<Recipe> (request.RecipeToAdd);
        _db.Recipes.Add(recipe);

        await _publishEndpoint.Publish<IRecipeAdded>(new
        {
            RecipeId = recipe.Id
        });

        await _db.SaveChangesAsync();

        return await _db.Recipes
            .ProjectTo<RecipeDto>(_mapper.ConfigurationProvider)
            .FirstOrDefaultAsync(r => r.Id == recipe.Id);
    }
}

I have a test that looks like this and passed before injecting IPublishEndpoint:

[Test]
public async Task can_add_new_recipe_to_db()
{
    // Arrange
    var fakeRecipeOne = new FakeRecipeForCreationDto { }.Generate();

    // Act
    var command = new AddRecipe.AddRecipeCommand(fakeRecipeOne);
    var recipeReturned = await SendAsync(command);
    var recipeCreated = await ExecuteDbContextAsync(db => db.Recipes.SingleOrDefaultAsync());

    // Assert
    recipeReturned.Should().BeEquivalentTo(fakeRecipeOne, options =>
        options.ExcludingMissingMembers());
    recipeCreated.Should().BeEquivalentTo(fakeRecipeOne, options =>
        options.ExcludingMissingMembers());
}

But when the MediatR command includes the MT publish, the tests fail:

 ----> System.InvalidOperationException : Unable to resolve service for type 'MassTransit.IPublishEndpoint' while attempting to activate 'RecipeManagement.Domain.Recipes.Features.AddRecipe Handler'.

Where I'm At

Again, this MediatR command works fine when running the actual app against RMQ, but it would seem that the in memory harness doesn't bring in the bus to properly inject IPublishEndpoint

I've tried bringing in something like this with various flavors and no avail.

var bus = _provider.GetRequiredService<IBus>();
services.AddSingleton<IPublishEndpoint>(bus);

I am thinking I could spin up a docker container with RMQ in it and just use that for my tests, but it would be nice if I could use the in memory harness for performance and simplicity.

Are there any thoughts on how I should be handling this?

CodePudding user response:

IPublishEndpoint is registered in the container, just like all the other bus-related interfaces, regardless of whether you use RabbitMQ or the InMemory transport.

IPublishEndpoint is scoped, not sure if you have a valid scope when you're resolving the dependencies in your scenario, but since it seems unchanged I would guess you might. But worth verifying.

CodePudding user response:

So the in memory db doesn't seem like it has a way to register the IPublishEndpoint like it can for consumers:

_provider = services.AddMassTransitInMemoryTestHarness(cfg =>
   {
      cfg.AddConsumer<AddToBook>();
      cfg.AddConsumerTestHarness<AddToBook>();
   }).BuildServiceProvider();
_harness = _provider.GetRequiredService<InMemoryTestHarness>();

but what i was able to do was just use Moq to add a mock of it. I don't need to test what happens after the publish at this level since it's an out of process operation, so this gets the registration job done to make my DI happy.

services.AddScoped(_ => Mock.Of<IPublishEndpoint>());
  • Related