Forem

Cover image for Consumer Driven Contract Testing - Hosting A Mocked API Using Azure Functions
Christian Eder
Christian Eder

Posted on

2 1

Consumer Driven Contract Testing - Hosting A Mocked API Using Azure Functions

Applying the concept of Consumer Driven Contract Testing is useful when you are building a system that is composed of multiple decoupled services. Some of these services will provide APIs, others will consume these APIs. Consumer Driven Contract Testing is about the API consumer publishing their expectations on the API in a way that allows

  • the provider of the API to verify that that it fulfils these expectations
  • the consumer of the API to easily stand up a mock of the provider API to run their own tests against

If you are not already familiar with the concept, you can dive deeper into the topic by reading Frank Rosner's excellent article or visit https://pact.io/. PACT is one of the major libraries available to implement Consumer Driven Contract Tests. The examples shown in this article however are based on the Impact library.

The basic idea is, that the consuming service defines a so called Pact that codifies the consumers expectations on the API structure & behavior:

Pact = new Pact();
var pathFormat = new Regex("weatherforecast\\/([a-zA-Z]+)\\/(\\d+)");
Pact.Given("")
.UponReceiving("A weather forecast request")
.With(new HttpRequest
{
Method = System.Net.Http.HttpMethod.Get,
Path = "weatherforecast/Munich/3"
})
.WithRequestMatchingRule(r => r.Path, r => r.Regex(pathFormat.ToString()))
.WillRespondWith(request =>
{
var match = pathFormat.Match(request.Path);
return new HttpResponse
{
Status = System.Net.HttpStatusCode.OK,
Body = new WeatherForecast
{
City = match.Groups[1].Value,
Date = new DateTime(2020, 3, 24),
Summary = "Sunny",
TemperatureC = 24,
TemperatureF = 75
}
};
})
.WithResponseMatchingRule(r => ((WeatherForecast)r.Body), r => r.Type());

The Pact shown in the example above defines an HTTP API that responds to any GET request to an URL with a path matching weatherforecast\/([a-zA-Z]+)\/(\d+) and responds with weather forecast data.

Based on this Pact, the consumer can stand up a test server on localhost and run tests of the consuming service using this mocked API:

var server = new HttpMockServer(pact, new JsonPayloadFormat());
server.Start();
HttpClient client = server.CreateClient();
var response = await httpClient.GetAsync("weatherforecast/berlin/0");

After successfully running its own tests against this mocked API, the consumer can publish the PACT to the provider as a JSON file:

string pactJson = pact.ToPactFile("Consumer Name", "Provider Name", server.TransportFormat);
view raw PactPublish.cs hosted with ❤ by GitHub

The provider can now take this JSON file and run the expectations defined in that file against its own API:

var client = new HttpClient();
// The address of the provider API to be tested
client.BaseAddress = new Uri("http://localhost:60374/");
var pact = new Pact(pactJson, new HttpTransport(client, new JsonPayloadFormat()), s => Task.CompletedTask);
var result = await pact.Honour();
Assert.True(result.Success, string.Join(Environment.NewLine, result.Results.Select(r => r.FailureReason)));

Hosting the mocked API on an Azure Function

If the consuming service does not want to run the mocked provider API on localhost - e.g. because it wants to run its own tests against a cloud hosted environment - we need a way to host the MockServer in the cloud as well. The following code snippet shows a fully working example of an Azure Function that will host an HTTP endpoint which will respond to any request as defined in the Pact:

public static class ProviderMock
{
[FunctionName("providerMock")]
public static async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Function, Route = "{*route}")] HttpRequest req,
string route)
{
var requestBody = new MemoryStream();
await req.Body.CopyToAsync(requestBody);
requestBody.Seek(0, SeekOrigin.Begin);
var request = new Core.Transport.Http.HttpRequest
{
Method = new HttpMethod(req.Method),
Path = route,
Query = req.QueryString.Value,
Body = PactDefinition.PayloadFormat.Deserialize(requestBody)
};
if (req.Headers.Any())
{
request.Headers = request.Headers ?? new Dictionary<string, string>();
foreach (var header in req.Headers)
{
request.Headers[header.Key] = header.Value;
}
}
Core.Transport.Http.HttpResponse response;
try
{
var interaction = PactDefinition.Pact.GetMatchingInteraction(request, PactDefinition.TransportMatchers);
if (request.Body is JObject jsonBody && interaction.RequestType != typeof(JObject))
{
request.Body = PactDefinition.PayloadFormat.Deserialize(jsonBody, interaction.RequestType);
}
response = (Core.Transport.Http.HttpResponse)interaction.Respond(request, PactDefinition.TransportMatchers);
}
catch (Exception ex)
{
return new NotFoundObjectResult("Getting a response from the pact failed for this request. Exception: " + Environment.NewLine + ex);
}
if (response.Headers != null)
{
foreach (var header in response.Headers)
{
req.HttpContext.Response.Headers.Add(header.Key, header.Value);
}
}
if (response.Body != null)
{
var memoryStream = new MemoryStream();
PactDefinition.PayloadFormat.Serialize(response.Body,memoryStream);
return new FileContentResult(memoryStream.ToArray(),
string.IsNullOrEmpty(PactDefinition.PayloadFormat.MimeType)
? "application/octet-stream"
: PactDefinition.PayloadFormat.MimeType);
}
else
{
return new StatusCodeResult((int)response.Status);
}
}
}

You can find the whole example (and more) on my GitHub repo

Top comments (0)