使用WebApplicationFactory<Startup>配置MassTransit进行测试

4
我有一个ASP.NET Core网站应用程序和测试设置,使用WebApplicationFactory测试我的控制器操作。之前我使用过RawRabbit,很容易模拟IBusClient并将其添加到DI容器中作为单例。在WebApplicationFactory<TStartup>.CreateWebHostBuilder()中,我调用这个扩展方法来添加已模拟的IBusClient实例,像这样;
/// <summary>
/// Configures the service bus.
/// </summary>
/// <param name="webHostBuilder">The web host builder.</param>
/// <returns>A web host builder.</returns>
public static IWebHostBuilder ConfigureTestServiceBus(this IWebHostBuilder webHostBuilder)
{
    webHostBuilder.ConfigureTestServices(services =>
    {
        services.AddSingleton<IBusClient, MY_MOCK_INSTANCE>
    });

    return webHostBuilder;
}

目前 RawRabbit 存在一些缺陷,这使我决定转向 MassTransit。但是,我想知道是否已经有更好的方法将 IBus 注册到容器中,而不必在测试中模拟它。不确定 InMemoryTestFixtureBusTestFixture 或者 BusTestHarness 是否能解决我的问题。也不确定它们如何一起使用以及它们的功能。

顺便说一下,在我的 ASP.NET Core 应用程序中,我有一个可重复使用的扩展方法设置,如下所示的代码,可以在启动时连接到 RabbitMQ。

/// <summary>
/// Adds the service bus.
/// </summary>
/// <param name="services">The services.</param>
/// <param name="configurator">The configurator.</param>
/// <returns>A service collection.</returns>
public static IServiceCollection AddServiceBus(this IServiceCollection services, Action<IServiceCollectionConfigurator> configurator)
{
    var rabbitMqConfig = new ConfigurationBuilder()
        .AddJsonFile("/app/configs/service-bus.json", optional: false, reloadOnChange: true)
        .Build();

    // Setup DI for MassTransit.
    services.AddMassTransit(x =>
    {
        configurator(x);

        // Get the json configuration and use it to setup connection to RabbitMQ.
        var rabbitMQConfig = rabbitMqConfig.GetSection(ServiceBusOptionsKey).Get<RabbitMQOptions>();

        // Add bus to the container.
        x.AddBus(provider => Bus.Factory.CreateUsingRabbitMq(cfg =>
        {
            cfg.Host(
                new Uri(rabbitMQConfig.Host),
                hostConfig =>
                {
                    hostConfig.Username(rabbitMQConfig.Username);
                    hostConfig.Password(rabbitMQConfig.Password);
                    hostConfig.Heartbeat(rabbitMQConfig.Heartbeat);
                });

            cfg.ConfigureEndpoints(provider);

            // Add Serilog logging.
            cfg.UseSerilog();
        }));
    });

    // Add the hosted service that starts and stops the BusControl.
    services.AddSingleton<IMessageDataRepository, EncryptedMessageDataRepository>();
    services.AddSingleton<IEndpointNameFormatter, EndpointNameFormatter>();
    services.AddSingleton<IBus>(provider => provider.GetRequiredService<IBusControl>());
    services.AddSingleton<IHostedService, BusHostedService>();

    return services;
}

2
另外,FYI,这已经被MT添加了:services.AddSingleton<IBus>(provider => provider.GetRequiredService<IBusControl>()); - Chris Patterson
1
@PlatypusMaximus,你解决了这个问题吗?我也和你当时处于同样的情况。我正在使用WebApplicationFactory,我的API启动程序有MassTransit配置。我需要用测试工具替换该配置,但我不知道如何操作,也找不到任何在线资源来指导我。如果能得到帮助,将不胜感激! - Frank Hale
@frank-hale,你解决了吗? - mslot
@mslot,我已经成功让它工作了。稍后我会在这里发布我的存储库。我需要在 GH 上设置它。 - secretAgentB
@RyanMAd 请执行!! - mslot
显示剩余4条评论
4个回答

3
在启动期间定义的 MassTransit 配置可以通过从 MassTransit 命名空间中删除服务并使用自定义 WebApplicationFactory 替换为新配置,例如:
public class CustomWebApplicationFactory : WebApplicationFactory<Startup>  
{   
    protected override void ConfigureWebHost(IWebHostBuilder builder) 
    {
      builder.ConfigureServices(services => 
      {
          var massTransitHostedService = services.FirstOrDefault(d => d.ServiceType == typeof(IHostedService) &&
                    d.ImplementationFactory != null &&
                    d.ImplementationFactory.Method.ReturnType == typeof(MassTransitHostedService)
                );
           services.Remove(massTransitHostedService);
           var descriptors = services.Where(d => 
                  d.ServiceType.Namespace.Contains("MassTransit",StringComparison.OrdinalIgnoreCase))
                                     .ToList();
         foreach (var d in descriptors) 
         {
           services.Remove(d);
         }     

         services.AddMassTransitInMemoryTestHarness(x =>
         {
           //add your consumers (again)
         });
      });
   }
}

那么你的测试可能看起来像这样

public class TestClass : IClassFixture<CustomApplicationFactory> 
 {
    private readonly CustomApplicationFactoryfactory;

    public TestClass(CustomApplicationFactoryfactory)
    {
      this.factory = factory;
    }

    [Fact]    
    public async Task TestName()
    {   
      CancellationToken cancellationToken = new CancellationTokenSource(5000).Token;  
      var harness = factory.Services.GetRequiredService<InMemoryTestHarness>();
      await harness.Start();

      var bus = factory.Services.GetRequiredService<IBusControl>();
      try
      {
        await bus.Publish<MessageClass>(...some message...);      
        
        bool consumed = await harness.Consumed.Any<MessageClass>(cancellationToken);
        //do your asserts
      }
      finally
      {
        await harness.Stop();
      }
    }   
  }

1
我最终在我的WebApplicationFactory内部创建了一个方法,如下所示;
    public void ConfigureTestServiceBus(Action<IServiceCollectionConfigurator> configurator)
    {
        this._configurator = configurator;
    }

让我能够在我的集成类构造函数内注册测试处理程序;
    public Intg_GetCustomers(WebApplicationTestFactory<Startup> factory)
        : base(factory)
    {
        factory.ConfigureTestServiceBus(c =>
        {
            c.AddConsumer<TestGetProductConsumer>();
        });
    }

当我调用扩展方法添加MassTransit的InMemory实例时,会使用此配置器。
public static IWebHostBuilder ConfigureTestServiceBus(this IWebHostBuilder webHostBuilder, Action<IServiceCollectionConfigurator> configurator)
{
    return webHostBuilder
        .ConfigureTestServices(services =>
        {
            // UseInMemoryServiceBus DI for MassTransit.
            services.AddMassTransit(c =>
            {
                configurator?.Invoke(c);

                // Add bus to the container.
                c.AddBus(provider =>
                {
                    var control = Bus.Factory.CreateUsingInMemory(cfg =>
                    {
                        cfg.ConfigureEndpoints(provider);
                    });

                    control.Start();

                    return control;
                });
            });

            services.AddSingleton<IMessageDataRepository, InMemoryMessageDataRepository>();
        });
}

1
但是,您如何将应用程序中注册的总线替换为内存中的总线?因为您只能调用一次 services.AddMassTransit - Marius Stănescu

0
你最好使用InMemoryTestHarness,这样可以确保你的消息协议可以被序列化,你的消费者已经正确配置,并且一切都正常工作。虽然有些人可能会称其为集成测试,但它真的只是在进行适当的测试。而且由于所有内容都在内存中,所以非常快速。
你可以点击此处查看一个单元测试示例,但下面也有一个简短的例子。
[TestFixture]
public class When_a_consumer_is_being_tested
{
    InMemoryTestHarness _harness;
    ConsumerTestHarness<Testsumer> _consumer;

    [OneTimeSetUp]
    public async Task A_consumer_is_being_tested()
    {
        _harness = new InMemoryTestHarness();
        _consumer = _harness.Consumer<Testsumer>();

        await _harness.Start();

        await _harness.InputQueueSendEndpoint.Send(new A());
    }

    [OneTimeTearDown]
    public async Task Teardown()
    {
        await _harness.Stop();
    }

    [Test]
    public void Should_have_called_the_consumer_method()
    {
        _consumer.Consumed.Select<A>().Any().ShouldBe(true);
    }


    class Testsumer :
        IConsumer<A>
    {
        public async Task Consume(ConsumeContext<A> context)
        {
            await context.RespondAsync(new B());
        }
    }


    class A
    {
    }


    class B
    {
    }
}

1
我认为在测试单个组件时这很有用。但是InMemoryTestHarness是否可以与WebApplicationFactory一起用于集成测试呢? - secretAgentB
我有一个集成测试,它从WebApplicationFactory运行我的ASP.NET Core Web应用程序实例,然后通过HttpClient执行GETPOSTPUTDELETE。上面的示例说明了如何模拟RawRabbit中的IBusClient,然后使用IWebHostBuilder.ConfigureTestServices方法将其注册到我的容器中。我想知道是否应该对IBus做同样的事情。我只是想知道是否已经有这样的东西可用。 - secretAgentB
我可能会创建一个新版本的AddServiceBus,类似于上面使用In-Memory Bus的那个。 - secretAgentB
我想我并不完全理解InMemoryBus是什么以及如何使用它。 - secretAgentB
3
@PlatypusMaximus,你解决了这个问题吗?我也和你当时处于同样的情况。我正在使用WebApplicationFactory,我的API启动程序有MassTransit配置。我需要用测试工具替换该配置,但我不知道如何操作,也找不到任何在线资源来指导我。如果能得到帮助,将不胜感激! - Frank Hale
显示剩余4条评论

0
在我的情况下(我只注入了IPublishEndpoint接口),我只需在ConfigureTestServices方法中注册另一个IPublishEndpoint,如下所示:
[TestClass]
public class TastyTests
{
    private readonly WebApplicationFactory<Startup> factory;
    private readonly InMemoryTestHarness harness = new();

    public TastyTests()
    {
        factory = new WebApplicationFactory<Startup>().WithWebHostBuilder(builder =>
        {
            builder.ConfigureTestServices(services =>
            {
                services.AddSingleton<IPublishEndpoint>(serviceProvider =>
                {
                    return harness.Bus;
                });
            });
        });
    }

    [TestMethod]
    public async Task Test()
    {
        await harness.Start();
        try
        {
            var client = factory.CreateClient();
            const string url = "/endpoint-that-publish-message";

            var content = new StringContent("", Encoding.UTF8, "application/json");
            var response = await client.PostAsync(url, content);
            (await harness.Published.Any<IMessage>()).Should().BeTrue();
        }
        finally
        {
            await harness.Stop();
        }
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接