Как настроить сервисы в 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);
}
}
}
}