使用 InMemoryTestHarness 时如何注入 MassTransit 的 IPublishEndpoint

How to inject MassTransit's IPublishEndpoint when using InMemoryTestHarness

概述

所以我有一个使用 MediatR 命令的现有网站 api。我正在努力将 MT 集成到其中一些中,以出于各种目的将消息发布到 RMQ。我能够很好地将它集成到我的命令中,但是在我的集成测试期间使用 InMemoryHarness 进行测试时,我 运行 遇到了问题。

详情

我有一个 NUnit 测试夹具,可以像这样设置我的内存线束,就像 the docs 描述。

[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();
}

我还有一个 MediatR 处理程序,可能看起来像这样:

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

我有一个看起来像这样的测试并在注入之前通过了 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());
}

但是当 MediatR 命令包含 MT 发布时,测试失败:

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

我在哪里

同样,当 运行 针对 RMQ 的实际应用程序时,此 MediatR 命令工作正常,但似乎内存中的线束 没有引入总线以正确注入 IPublishEndpoint

我试过把类似这样的各种口味的东西带进来,但没有用。

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

我想我可以启动一个 docker 容器,其中包含 RMQ,并将其用于我的测试,但如果我可以使用内存中的线束来提高性能和简单性,那就太好了。

关于我应该如何处理这个问题有什么想法吗?

IPublishEndpoint 注册在容器中,就像所有其他与总线相关的接口一样,无论您使用 RabbitMQ 还是 InMemory 传输。

IPublishEndpoint 是范围内的,当您在解决方案中的依赖项时不确定您是否具有有效范围,但由于它似乎没有变化,我猜您可能会。但值得验证。

已更新

所以下面的方法可以工作,但是它不会让你从发布的角度对 harness 进行任何测试,这显然不理想,如果你像这样设置你的注册,它现在应该会飞:

        services.AddMassTransitInMemoryTestHarness(cfg =>
        {
            // Consumer Registration -- Do Not Delete Comment

            cfg.AddConsumer<AddToBook>();
            cfg.AddConsumerTestHarness<AddToBook>();
        });
        
        _provider = services.BuildServiceProvider();
        _scopeFactory = _provider.GetService<IServiceScopeFactory>();
        
        _harness = _provider.GetRequiredService<InMemoryTestHarness>();
        await _harness.Start();

所以内存数据库似乎没有办法像消费者那样注册 IPublishEndpoint:

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

但我能做的只是使用 Moq 添加它的模拟。我不需要测试在这个级别发布后会发生什么,因为它是进程外操作,所以这完成了注册工作,让我的 DI 开心。

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