Understand Contract Testing and How to implement it in C#

Zack Yang
4 min readSep 14, 2023

--

In software development, ensuring the reliability and stability of microservices and APIs has become paramount. As applications grow in complexity, so does the need for robust testing strategies that can help teams deliver high-quality code without sacrificing agility. One approach that has gained significant traction in recent years is Contract Testing. In this article, I’m going to demystify contract testing and show you how to implement it in your C# projects.

1. Terms

Consumer: the code that consumes the service, usually refers to the client.

Provider: the code that provide the service, usually refers to the server.

Contract: the agreed protocol between consumers and providers. It includes expected requests (input) and response (output).

2. Why contract testing?

Building and maintaining microservices can be a daunting task. In a world where numerous services must seamlessly interact with one another, ensuring that changes to one service do not break the functionality of another is a challenge. Traditional integration testing tests the interactions between entire systems, which is too heavy and too slow, even worse, it cannot identify the issue straightforwardly. In contrast, contract testing focuses on the contracts between individual services. Contract testing tests the consumer and the provider separately based on the agreed contract between the consumer and provider.

3. How to perform the contract testing?

In contract testing, the consumer side programmer writes “consumer tests”, which consist of expected input and output, and the expectation will be saved into Pact Json files. When running, the tests send requests to the built-in mock server instead of the real server, and the mock server uses the saved Pact Json files to send response, which will be used to verify consumer side test cases.

Furthermore, contract testing framework will read the saved Pact Json files, and send request to service provider (server), and the response will be verified against the expected output in the Pact Json files.

4. What is Pact?

Pact is an implementation of contract testing. As consumer and provider may be developed using different programming languages, so Pact is language-independent, and it supports many programming languages, such as Java, .NET, Ruby, JavaScript, Python, PHP, etc. The saved Pact Json files produced by the consumer developed in one programming language can be used to verify the provider developed in another programming language.

In this article, the consumer and the provider are both developed using .NET (C#). Pact.Net is the implementation of Pact in .Net.

5. How to use Pact.Net?

Works are divided into three parts: developing WebAPI service to be tested; writing consumer tests; writing provider tests;

a. Developing WebAPI service

Create an ASP.Net Core WebAPI project, and write a simple controller as follows:

[ApiController]
[Route("[controller]/[action]")]
public class MyController : ControllerBase
{
[HttpGet]
public int Abs(int i)
{
return Math.Abs(i);
}
}

This controller provides a simple service, which can calculate the absolute value of a given integer.

Pact should use the Startup type of .Net application to start up the webserver, however, in latest .NET Core, the traditional Startup.cs is replaced by Minimal API. To used Pact with .NET Core, you have to switch to Startup style, and if you have no idea about it, please Google “Adding Back the Startup Class to ASP.NET Core”.

b. writing consumer tests

Create a .NET Core test project with xUnit, and then install the Nuget package “PactNet” to it.

Add a test as follows:

public class UnitTest1
{
private readonly IPactBuilderV4 pactBuilder;
public UnitTest1()
{
var pact = Pact.V4("MyAPI consumer", "MyAPI",new PactConfig());
this.pactBuilder = pact.WithHttpInteractions();
}
[Fact]
public async Task Test1()
{
this.pactBuilder.UponReceiving("A request to calc Abs")
.Given("Abs")
.WithRequest(HttpMethod.Get, "/My/Abs")
.WithQuery("i","-2")//Match.Integer(-2)
.WillRespond()
.WithStatus(HttpStatusCode.OK)
.WithJsonBody(2);

await this.pactBuilder.VerifyAsync(async ctx=>
{
using HttpClient httpClient = new HttpClient();
httpClient.BaseAddress = ctx.MockServerUri;
var r = await httpClient.GetFromJsonAsync<int>($"/My/Abs?i=-2");
Assert.Equal(2,r);
});
}
}

The “WithRequest().WithQuery()” is to define input, and “WillRespond().WithJsonBody()” is to define the corresponding expected output.

The code snippets in VerifyAsync is test cases, which is tested against the defined expectation by “UponReceiving”. As can be seen from “httpClient.BaseAddress = ctx.MockServerUri”, the provider test case interacts with mock server provided by Pact instead of the real server.

Let’s run the test, a “MyAPI consumer-MyAPI.json” will be generated in the folder “pact” in the test project, as shown below. The expected input and output are saved in this Json file.

c. writing provider tests

Create a .NET Core test project with xUnit, and then install the Nuget package “PactNet” and “PactNet.Output.Xunit” to it. Because the provider test must launch the tested server using the Startup class, so please add reference of the tested ASP.NET Core WebAPI project to the provider tests project.

Create a “MyApiFixture” class as follows for starting up the tested WebAPI server in the test project.

public class MyApiFixture: IDisposable
{
private readonly IHost server;
public Uri ServerUri { get; }
public MyApiFixture()
{
ServerUri = new Uri("http://localhost:9223");
server = Host.CreateDefaultBuilder()
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseUrls(ServerUri.ToString());
webBuilder.UseStartup<Startup>();
})
.Build();
server.Start();
}

public void Dispose()
{
server.Dispose();
}
}

Next, add a test to test the server using saved Pact Json files as follows:

public class MyApiTest1: IClassFixture<MyApiFixture>
{
private readonly MyApiFixture fixture;
private readonly ITestOutputHelper output;
public MyApiTest1(MyApiFixture fixture,ITestOutputHelper output)
{
this.fixture = fixture;
this.output = output;
}
[Fact]
public async Task Test1()
{
var config = new PactVerifierConfig
{
Outputters = new List<IOutput>
{
new XunitOutput(output),
},
};
string pactPath = Path.Combine("..","..","..","..",
"TestConsumerProject1", "pacts", "MyAPI consumer-MyAPI.json");
using var pactVerifier = new PactVerifier("MyAPI", config);
pactVerifier
.WithHttpEndpoint(fixture.ServerUri)
.WithFileSource(new FileInfo(pactPath))
.Verify();
}
}

The “pactPath” refers to the saved Pact file, which varies in your projects with difference project names and different relative paths.

When the above test is executed, Pact will launch the tested server in test project and send requests and verify responses against the saved Json file, which contains the expected inputs and outputs from consumer.

6. Pact for messaging

Pact support messaging-based async API or messaging as well. Please see the “messaging pacts” sections of Pact’ documentation.

--

--

Zack Yang
Zack Yang

No responses yet