1

In a microservice environment I need to construct a framework for Contract based testing. I'm currently investigatingh how to isolate a single service from it's external dependencies inorder to execute the Provider tests.

What I need to do is:

  • Keep the WebApi project intact
  • Start an instance of the WepApi with some config-differences
  • Mock out selected dependencies

My Solution structure is this:

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

I've read this guide about Pact Tests in dotnet. Focusing on Case.Pace.ProviderTest, I need to start Case.Api programmatically from Case.Pact.ProviderTest (and another WebHost for Pact it self) and substitute some of it dependencies.

So far I got this:

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
}

In the line containing .UseStartup<CaseStartup>() I've simply copied Startup.cs from Case.Api and changed the needed dependencies, which works fine.

But I want a more generic solution. It's doesn't feel right to just copy code and call it a day :) because it is not generic and reusable for the other services.

So I kept digging, and came up with the following.

Add Controllers from other assembly
I realised, that starting a IWebhost, using StartUp from a different assembly, doesn't automagically add controller from that assembly. This needs to be done explicitly. So I did this:

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

Awesome!!! So far so good.

Next issue:

Replace dependencies:
Reding this article, I created the extension method for replacing dependencies:

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

So I'm able to replace the dependencies I want like so: (in this case a QueryHandler)

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

But this doesn't solve my issue with copied code.

My wet dream is to be able to use Startup.cs from Case.Api and somehow tweak the DI to replace the dependencies, without having all the redundant code.

Any input would be highly appriciated.
Thanks :)

terle
  • 86
  • 10

1 Answers1

0

I had a similar situation also using Pact.net. But I wanted to use TestServer, unfortunately Pact.net does not have support for httpClient ( Verifying pact with in-memory API ). At the end I use a combination of two libraries, probably not the best to validate all scenarios. I took the consumer part of Pact.net to generate the contract and the Verifier part of Pactify to verify if the provider is honoring the contract. The Verifier requires code modification to be compatible with the Pact.net contract though.

I also used your code example to replace dependencies for mocks using 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://learn.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 : here I use your extension to replace dependencies

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

    }

}
  • Hi Isreal, So awesome you took the time to answer this question. I'm really sorry to say that I don't have access to the source anymore and therefor cannot verify your answer :( especially when you spent so much time with this. Hope you'll keep it up :) Welcome to the community – terle Sep 12 '21 at 10:45