Как настроить сервисы в WebAPI ядра Asp.Net из другой сборки

В среде микросервисов мне нужно создать структуру для тестирования на основе контрактов. В настоящее время я изучаю, как изолировать отдельную службу от ее внешних зависимостей для выполнения тестов провайдера.

Что мне нужно сделать:

  • Сохраните проект WebApi без изменений
  • Запустите экземпляр WepApi с некоторыми отличиями в конфигурации
  • Макет выбранных зависимостей

Моя структура решения такова:

Case-Solution/  
├── src/  
|   ├──Case.Api  
|   └──Case.Application  
├── test/  
|   ├──Case.Api.Unittest  
|   ├──(other tests)  
|   ├──Case.Pact.CunsumerTest  
|   └──Case.Pact.ProviderTest  

Я прочитал это руководство о тестах Pact в dotnet. Сфокусироваться наCase.Pace.ProviderTest, Мне нужно начать Case.Api программно из Case.Pact.ProviderTest (и еще один WebHost для самого Пакта) и замените некоторые из его зависимостей.

Пока я получил это:

public class ProviderApiTests : IDisposable
{
    private string ProviderUri { get; }
    private string PactServiceUri { get; }
    private IWebHost PactServiceWebHost { get; }
    private IWebHost CasesWebHost { get; }
    private ITestOutputHelper OutputHelper { get; }
    public static IConfiguration CaseConfiguration { get; } = new ConfigurationBuilder()
        .SetBasePath(Directory.GetCurrentDirectory())
        .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
        .AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT" ?? "Production"}.json", optional: true)
        .AddEnvironmentVariables()
        .Build();


    public ProviderApiTests(ITestOutputHelper output)
    {
        OutputHelper = output;
        ProviderUri = "http://localhost:9000";
        PactServiceUri = "http://localhost:9001";

        CasesWebHost = WebHost.CreateDefaultBuilder()
            .UseUrls(ProviderUri)
            .UseStartup<CaseStartup>()
            .UseConfiguration(CaseConfiguration)
            .Build();
        CasesWebHost.Start();

        PactServiceWebHost = WebHost.CreateDefaultBuilder()
            .UseUrls(PactServiceUri)
            .UseStartup<ProviderServerStartup>()
            .Build();
        PactServiceWebHost.Start();
    }

    [Fact]
    public void EnsureProviderApiHonoursPactWithConsumer()
    {
        //Arrange
        var config = new PactVerifierConfig
        {
            Outputters = new List<IOutput>
            {
                new XUnitOutput(OutputHelper)
            },
            Verbose = true,
            CustomHeader = new KeyValuePair<string, string>("X-apikey", "XXX")
        };
        //Act //Assert
        IPactVerifier pactVerifier = new PactVerifier(config);
        pactVerifier.ProviderState($"{PactServiceUri}/provider-states")
            .ServiceProvider("CaseProvider", ProviderUri)
            .HonoursPactWith("CaseConsumer")
            .PactUri(@"..\..\..\..\..\pacts\caseconsumer-caseprovider.json")
            .Verify();
    }
    #region IDisposable Support
    //IDisposable code
    #endregion
}

В строке, содержащей .UseStartup<CaseStartup>() Я просто скопировал Startup.cs от Case.Api и изменил необходимые зависимости, что отлично работает.

Но мне нужно более общее решение. Кажется неправильным просто копировать код и прекращать работу:), потому что он не является универсальным и не может использоваться повторно для других сервисов.

Я продолжал копать и пришел к следующему.

Добавление контроллеров из другой сборки
Я понял, что запуск IWebhost с использованием StartUp из другой сборки не добавляет автоматически контроллер из этой сборки. Это нужно делать явно. Итак, я сделал это:

public void ConfigureServices(IServiceCollection services)
{
    var assembly = Assembly.Load("Case.Api");
    services.AddMvc()
       .SetCompatibilityVersion(CompatibilityVersion.Version_2_1)
       .AddApplicationPart(assembly)
       .AddControllersAsServices();
    ......
}

Потрясающе!!! Все идет нормально.

Следующий выпуск:

Заменить зависимости:
Перерисовывая эту статью, я создал метод расширения для замены зависимостей:

public static void Replace<TRegisteredType>(this IServiceCollection services, TRegisteredType replcement)
{
    for (var i = 0; i < services.Count; i++)
    {
        if (services[i].ServiceType == typeof(TRegisteredType))
        {
            services[i] = new ServiceDescriptor(typeof(TRegisteredType), replcement);
        }
    }
}

Итак, я могу заменить зависимости, которые хочу, вот так: (в данном случае QueryHandler)

public void ConfigureServices(IServiceCollection services)
{
    .....
    var queryHandler = Substitute.For<IQueryHandler<Query, QueryResult>>();
    queryHandler.Handle(Arg.Any<Query>()).Returns(new QueryResult(...));
    services.Replace(queryHandler);
    ......
}

Но это не решает мою проблему с скопированным кодом.

Моя влажная мечта - уметь пользоваться Startup.cs от Case.Api и каким-то образом настроить DI, чтобы заменить зависимости, не имея всего избыточного кода.

Любой вклад будет очень оценен.
Спасибо:)

1 ответ

У меня была аналогичная ситуация, когда я использовал Pact.net. Но я хотел использовать TestServer, к сожалению, Pact.net не поддерживает httpClient ( проверка пакта с API в памяти ). В конце я использую комбинацию двух библиотек, вероятно, не лучшую для проверки всех сценариев. Я взял потребительскую часть Pact.net для создания контракта и часть Verifier чтобы проверить, Pactify,соблюдает ли поставщик контракт. Однако Verifier требует модификации кода, чтобы он был совместим с контрактом Pact.net.

Я также использовал ваш пример кода, чтобы заменить зависимости для моков с помощью moq.

      [TestClass]
public class EndpointShouldHonorContract
{

    private HttpClient httpClient;
    private ApiWebApplicationFactory<Startup> testServerFactory;
    Mock<IRepository> repositoryMock =
        new Mock<IRepository>();

    public EndpointShouldHonorContract()
    {
        //omitting code... Creation of mock Data Set https://docs.microsoft.com/en-us/ef/ef6/fundamentals/testing/mocking?redirectedfrom=MSDN#testing-query-scenarios
        repositoryMock.Setup(s => s.GetQueryable()).Returns(mockDataSet.Object);

        testServerFactory = new ApiWebApplicationFactory<Startup>(services => 
        {
            services.Replace<IRepository>(repositoryMock.Object);
        });

        httpClient = testServerFactory.CreateClient();
    }

    [TestMethod]
    public async Task HonorContract() 
    {
          // this is my modified Pactify Verifier 
           await MyModifiedPactify
            .VerifyPact
            .PactVerifier
            .Create(httpClient)
            .Between("consumer", "provider")
            //location of contract, there is also an option where you can get contracts from a http location
            .RetrievedFromFile(@"\Pacts")
            .VerifyAsync();
    }
 }

Web Api Factory: здесь я использую ваше расширение для замены зависимостей

      public class ApiWebApplicationFactory<TStartUp>
    : WebApplicationFactory<TStartUp> where TStartUp: class
{

    Action<IServiceCollection> serviceConfiguration { get; }

    public ApiWebApplicationFactory(Action<IServiceCollection> serviceConfiguration) : base()
    {
        this.serviceConfiguration = serviceConfiguration;
    }

    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        if (this.serviceConfiguration != null)
        {
            builder.ConfigureServices(this.serviceConfiguration);
        }
    }


}

internal static class ServiceCollectionExtensions
{
    public static void Replace<TRegisteredType>(this IServiceCollection services, TRegisteredType replacement)
    {
        for (var i = 0; i < services.Count; i++)
        {
            if (services[i].ServiceType == typeof(TRegisteredType))
            {
                services[i] = new ServiceDescriptor(typeof(TRegisteredType), replacement);
            }
        }

    }

}
Другие вопросы по тегам