Microservices - Why wait for your Integration tests to fail when you have Pact?
Tags: microservices, pact, testing
Published: 2019-04-02 19:08:22

---
title: Microservices - Why wait for your Integration tests to fail when you have Pact?
published: true
description: In this post I try to put together my experience on how Pact helps us in writing better APIs for microservices.
tags: microservices, pact, testing
---

With more and more software moving from monolith to microservices, we are swamped in the world of more APIs than ever. How can we write APIs that are meaningful, precise , testable and maintainable? Once an API is out there we are obligated not to break them or alternatively we have to know before hand that we have breaking changes. 

In this post I try to put together my experience on how Pact (https://docs.pact.io) helps us in writing better APIs in our microservices architecture.

## What is a Pact?

> Pact (noun):
A formal agreement between individuals or parties.

According to [`Pact Foundation`](https://pact.io)

> Pact is a `contract testing tool`. Contract testing is a way to ensure that services (such as an API provider and a consumer) can communicate with each other. Without contract testing, the only way to know that services can communicate is by using expensive and brittle integration tests.

## Parties

Any API(Application Programming Interface) consists of two important parties:

__Provider:__ One who exposes one or more API(s)

__Consumer(s)__: One or more client(s) who uses the API(s) exposed by the Provider

## How does a Pact work?

`Consumer captures an expectation` as a separate pact, and `provider agrees to it`.

## Advantages of using Pacts

### Isolation:
  The Ability to test the provider and consumer **in Isolation but in conjunction**, i.e. Pacts gives us the sophistication of

  - testing a consumer with provider's API from the comfort of a developer's machine Without having to make an actual call.
  - testing a provider's API changes against one or more consumers to make sure that the changes do not accidentally break the existing consumers

### Quick Feedback
  We do not have to wait for our End to End tests to fail, the same feedback can be unit tested.

### User Centred API design
  Pacts are essentially `Consumer Driven Contracts`, so the consumers are laying out the expectation which leads to better usable APIs

### Overview of existing APIs
- Pacts provides `Network Graph` of dependant services
- Pacts helps us understand, if `one or more APIs are similar`
- Pacts can also show `unused APIs`, when the consumers are not using them anymore.

### Language independent
   Pact is a [specification](https://github.com/pact-foundation/pact-specification#index), which makes it perfect for microservices testing. You can find Pact consumer and provider libraries implemented in many programming languages [here](https://github.com/pact-foundation)

## Pact in Action:

### An Example Pact

Let us take the most simple use case of authentication as a service.

Consumer: I need to log the user in, I have user credentials
Provider: I can authenticate a user given credentials

For demonstration, we use the same pact as described above. Complete implementation can be found [here](https://github.com/shyamz-22/Pactit)

### Consumer Pact in Go:

```go
    import (
    	"fmt"
    	"github.com/pact-foundation/pact-go/dsl"
    	"net/http"
    	"testing"
    )

    func TestClient_AuthenticateUser(t *testing.T) {

    	var username = "alice"
    	var password = "s3cr3t"

    	t.Run("user exists", func(t *testing.T) {
    		pact := &dsl.Pact{
    		Consumer: "Quoki",
    		Provider: "UserManager",
    	  PactDir:  "../pacts",
    	  LogDir:   "../pacts/logs",
    	}

      defer pact.Teardown()

    		pact.
    			AddInteraction().
    			Given("user exists").
    			UponReceiving("a request to authenticate").
    			WithRequest(dsl.Request{
    				Method:  "POST",
    				Path:    dsl.String(fmt.Sprintf("/users/%s/authentication", username)),
    				Headers: dsl.MapMatcher{"Content-Type": dsl.Like("application/x-www-form-urlencoded")},
    				Body: dsl.MapMatcher{
    				"password": dsl.Like(password),
    				},
    			}).
    			WillRespondWith(dsl.Response{
    				Status: http.StatusNoContent,
    			})

    	pact.Verify(func() error {
    			subject := New(fmt.Sprintf("http://localhost:%d", pact.Server.Port))
    			ok := subject.AuthenticateUser(username, password)

    			if !ok {
    				t.Fail()
    			}

    			return nil
    		})

    	})
    }
```

Running this successfully will generate,

```json
    {
      "consumer": {
        "name": "Quoki"
      },
      "provider": {
        "name": "UserManager"
      },
      "interactions": [
        {
          "description": "a request to authenticate",
          "providerState": "user exists",
          "request": {
            "method": "POST",
            "path": "/users/alice/authentication",
            "headers": {
              "Content-Type": "application/x-www-form-urlencoded"
            },
            "body": "password=s3cr3t",
            "matchingRules": {
              "$.headers.Content-Type": {
                "match": "type"
              },
              "$.body.password": {
                "match": "type"
              }
            }
          },
          "response": {
            "status": 204,
            "headers": {
            }
          }
        }
      ],
      "metadata": {
        "pactSpecification": {
          "version": "2.0.0"
        }
      }
    }
```


### Provider verification of Pact in Kotlin

The Provider then takes this as requirement, then verifies by running this test

```kotlin
    package com.shyamz.provider.authenticate

    import au.com.dius.pact.provider.junit.Consumer
    import au.com.dius.pact.provider.junit.Provider
    import au.com.dius.pact.provider.junit.State
    import au.com.dius.pact.provider.junit.loader.PactFolder
    import au.com.dius.pact.provider.junit.target.HttpTarget
    import au.com.dius.pact.provider.junit.target.Target
    import au.com.dius.pact.provider.junit.target.TestTarget
    import au.com.dius.pact.provider.spring.SpringRestPactRunner
    import org.junit.Before
    import org.junit.runner.RunWith
    import org.springframework.beans.factory.annotation.Autowired
    import org.springframework.boot.test.context.SpringBootTest


    @RunWith(SpringRestPactRunner::class)
    @Provider("UserManager")
    @Consumer("Quoki")
    @PactFolder("../consumer/src/consumer/http/pacts")
    @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT,
            properties = ["server.port=8601"])
    class QuokiUserAuthenticatePactItTest {

        @Suppress("unused")
        @JvmField
        @TestTarget
        final val target: Target = HttpTarget(port=8601)

        @Autowired
        private lateinit var userRepository: UserRepository

        @Before
        fun setUp() {
            userRepository.deleteAll()
        }

        @State("user exists")
        fun userExists() {
            userRepository.save(QuokiUser(userName = "alice", password = "s3cr3t"))
        }
    }
```
Running this will provide the result

```
    Verifying a pact between Quoki and UserManager
    Given user exists
        a request to authenticate
    returns a response which
        has status code 204 (OK)
        has a matching body (OK)
```

## Best Practices

### A Bad Pact

A bad pact, dictates what it considers as a precondition for an invalid username and tightly couples the exact error message that is expected.

This is then unit testing the provider's implementation detail rather than the contract itself. So the provider cannot change the validation rule or the error message without breaking the consumer.

__Given :__ Alice does not exist

__Upon receiving:__ A request to create user, with user name that has special characters other than underscores

__Response:__ Is 400 Bad Request

__Response Body:__ {"error": "username cannot contain special characters"}

### A Good Pact

A Good pact from a consumer __hides the provider's implementation details. It must only capture expectation from consumer point of view__. In the below example a consumer shall not dictate what it considers an invalid username.

__Given :__ Alice does not exist

__Upon receiving:__ A request to create user with invalid username

__Response:__ Is 400 Bad Request

__Response Body:__ {"error": "[a non empty string]"}


### Conclusion

If you have more questions or if you need more information, you can find all the information [here](https://docs.pact.io/)