Intro to PACT for .NET Core: API contract testing

Ajay kumar
8 min readMar 27, 2024

--

Recently i came a across a situation where i had to explore contract testing using PACT framework.

For some background, the account that i was working for had already implemented contract testing in their micro-services. They were following schema based contract testing approach.

In short, schema based testing approach involves matching of the schema that consumers of an API are using against the said API provider. The example and images below will illustrate schema based testing using a small example scenario.

Imagine a microservices system with 3 consumers (A, B and C) for a Provider API. Now all three consumers are expecting a certain fields for their use case from the Provider API. As long as Provider is able to maintain the contract expectations of the consumers, things will be fine.

In schema based contract tests, the testing framework is responsible for getting the schemas of all the consumers and it also holds the current schema of the Provider service. The next step is the comparison which is show in images below.

Diagram 1: Success Case
Diagram 1: Success Case

In diagram above, a success scenario is shown in a schema based contract testing. All the tests are success as the expected fields by all the clients are available in provider’s schema.

Diagram 2: Failure Case
Diagram 2: Failure Case

The above diagram shows a failure case. The failure is because the Consumer C is expecting the role field which now has been removed from the Provider API schema.

Everything till here looks good, we are able to capture failures, then why we explored another options ?

Its because in the approach above, the contract test pipeline was running on development environment and the schema was taken from a currently deployed service in development environment. This means that the failure will only be caught after the breaking change has been introduced in the Provider API repository and deployed to certain environment. In short failure detection was late.

Diagram 3: Flow with schema based testing

As you can see above, the failure detection is after the build and deployment step were done.

We wanted the build step itself to fail so that an early detection of the breaking change is caught before it can make make any mess. Something like below.

Diagram 4: Failure at build step

How we achieved the above using PACT framework ?

I will focus on how i implemented consumer driven contract testing using PACT in .NET Core API. I will not go into the very details of what PACT is, as that is explained much better at https://docs.pact.io/.

We will dive into the code with below services setup:

  1. Student service (PactNet.Provider): Provider API
  2. Report Card Service (PactNet.ConsumerOne): Consumer API

Student service exposes below API:

Diagram 5: Student by Id API

I had the solution structure as below.

Diagram 6: Project structure

Consumer side tests:

The very first step is to create a unit test file and initialize pact builder as shown below

Diagram 7: Pact Builder Setup

Next we have to write test and setup scenario. Here below, we are instructing pact builder to return a specific response when it gets a specific request on the mock server (ctx.MockServerUri).

Diagram 8: Pact consumer unit test

The success test run will look like below

Diagram 9: Pact consumer test run

Once above test run, 2 things will happen.

First, the logic inside the StudentClient class will be tested to make sure the request that is dispatched from the system to the provider service should contain all the required request attributes that are defined in the unit test.

Second, a Pact file will be generated with the name ConsumerOne — Student API.json. The naming of the file can be defined at the time of pact builder initialization.

This pact file needs to be passed to Provider API so that it can be accessed inside unit test, for simplicity here, we have included this file as part of Provider API’s unit test project itself. However, there is much better way which provides many other features, that is called pactflow(https://pactflow.io/ ). It offers free plan as well which can be used to small or test projects.

The pact file will look something like below. This file contains, among other details, the request structure that ConsumerOne(Report Card API) will be sending and response that it expects from the Provider API

Diagram 10: Pact contract file

Provider side tests and verification of pact file:

Once we have the pact file with us, we can use this pact file to validate it against the Provider API.

Provider side tests take a little more effort than the consumer side tests, as it requires more setup.

Essentially what happens is that PACT tries to fire up an actual instance of the provider service and test the same request as mentioned in the pact file against this fired up instance and then matches the response to validate.

One must be wondering then, that actual instance will require other stuff as well, like a database, any third party APIs involved and many more. So what will happen with all that ? This is true, however, in case of Pact provider tests, all those things can be mocked. For database, if we do not want to mock, we can use in-memory databases as well. At the end, our target is to test the logic that resides in our code base that can possibly affect the nature of contract.

Mocking is done in the same way we mock normal unit tests. Hence, the provider contract tests will also be running like unit tests only. Provider unit tests structure looks like below.

Diagram 11: Files in provider tests

In the StudentApiFixture class we are trying to setup and start a host

Diagram 12: Provider API’s

In ProviderStateMiddleware class we are trying to setup the state of the system. It is to note that using Pact we can write complex test scenarios as well. For example lets say if student address anything other than Delhi then, consumer is expecting an additional field called City. These kind of scenarios can be handled by defining the state of the Provider API using state middlewares like below.

In the below middleware, we are adding a middleware that will add a student with id 1 in the system. If you correlate, then code (line #28) below is the same sentence that is mentioned in the pact file, provider state section.

Diagram 13: ProviderStateMiddleware class

Next step is to define the TestStartup(can be named anything) class. This class contains all the code that our Provider API needs to get started.

It is to note that an actual production application may contain various other services and middlewares like, logging, analytics, health checks etc, which does not affect the code logic. However, since our purpose is limited to testing the code that can potentially impact the contract side of things of the application, so we can ignore all other things, to make our tests lighter. This should be subset of the actual Startup.cs file in the Provider API. In short, just enough for our tests to run.

Also, since ours is a small test application we are not mocking the methods defined in IStudentRepo class, but we can mock them whenever required. The idea is that Pact test should be able to run in isolation, not depending on any actual service or any environment.

Diagram 14: TestStartup class

Finally the test case. In the test case, instruct Pact verifier about which fixture class to use for setting up and running the host (line 16) and along with the path of the Pact file that was generated by the consumer (line 39). Pact verifier take all the required information about host and pact file and finally runs Verify() method.

Diagram 15: Provider test class

The test run will look something like below.

Diagram 15: Provider test run for success case

Failure Scenario

To test out failure scenario, we will try to change the pact file manually and mimic a scenario such that the consumer is expecting another field called “classId” (line 26) which, at the moment, our provider API does not supports. After manually changing the pact file the response expected by consumer service will look like below:

Diagram 16: Changing pact file manually to mimic fail scenario

After this, if we try to get this pact file verified by our provider unit test, we will get a failure result indicating why the test failed. The failure message ($ -> Actual map is missing the following keys: classId) below indicates the field name that was not supported by the provider API but was expected by the consumer service.

Diagram 17: Failure result

Now, since the contract verification is happening at unit test level, we were able to handle the breaking changes detection at build level only. Even better, we can use pre-commit hook so that we get information on breaking changes at development level and faulty code is never committed to code repository.

For reference, the code repository being is discussed is available on github link: https://github.com/ajaysskumar/pact-net-example

Thanks for reading through. Please share feedback, if any, in comments or on my email ajay.a338@gmail.com.

--

--

Ajay kumar

Software enthusiast in architecture and testing. Movie and series lover. Welcome to my blog of professional and personal passions.