DEV Community

Shyamala
Shyamala

Posted on

Microservices - Why wait for your Integration tests to fail when you have Pact?

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

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, which makes it perfect for microservices testing. You can find Pact consumer and provider libraries implemented in many programming languages here

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

Consumer Pact in 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,

    {
      "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

    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

Discussion (0)