Contract testing is a technique for testing an integration point by checking each application in isolation to ensure the messages it sends or receives conform to a shared understanding that is documented in a "contract".

https://docs.pact.io/

The responsibility of the contract test is to validate the provider APIs based on the given contract. A service that produces data can change over time, it’s important that the contracts with each service that consumes data from it continue to meet expectations. Contract tests provide a mechanism to explicitly verify that a provider meets a contract.

Three main parties in the contract tests are

Consumer: An application or service that makes use of the functionality or data from another application to do its job.

Provider: An application or service that provides functionality or data for other applications to use via an API.

Pact Broker(Pactflow): The Pact Broker is an application for sharing the contracts and verification results between the consumer and the provider.

Boundary

Where to put the contract tests

under the folder /ctest

How to Write Contract Test

Consumer Pact Test

  • Write test for XXXClient which interact with external service

  • Annotated with @Tag("contract") and @ExtendWith(PactConsumerTestExt::class)

  • Stub external server using @PactTestFor(providerName = "provider-service-name", port = "PORT")

  • Each contract test consist of two part

    • Definition of contract which is annotated with @Pact(provider = "provider-service-name", consumer = "consumer-service-name")

    • A executable test (it generates a contract with json format)

CustomerIamClientTest
package ph.safibank.customermanager.client

import au.com.dius.pact.consumer.dsl.PactDslWithProvider
import au.com.dius.pact.consumer.junit5.PactConsumerTestExt
import au.com.dius.pact.consumer.junit5.PactTestFor
import au.com.dius.pact.core.model.RequestResponsePact
import au.com.dius.pact.core.model.annotations.Pact
import io.micronaut.test.extensions.junit5.annotation.MicronautTest
import jakarta.inject.Inject
import org.apache.http.entity.ContentType
import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertDoesNotThrow
import org.junit.jupiter.api.extension.ExtendWith
import java.util.UUID

@Tag("contract")
@MicronautTest
@ExtendWith(PactConsumerTestExt::class)
@PactTestFor(providerName = "iam-manager", port = "8073")
class CustomerIamClientTest {
    @Inject
    private lateinit var customerIamClient: CustomerIamClient

    @Pact(provider = "iam-manager", consumer = "customer-manager")
    fun `contract of getting customer credential id by customer id`(builder: PactDslWithProvider): RequestResponsePact? {
        return builder
            .given("credential exist")
            .uponReceiving("A request to get customer credential id by customer id")
            .path("/credential/by-customer/${customerId}")
            .method("GET")
            .willRespondWith()
            .status(200)
            .body(credentialBody, ContentType.APPLICATION_JSON)
            .toPact()
    }

    @Test
    fun `verify contract of getting customer credential id by customer id`() {
        assertDoesNotThrow {
            customerIamClient.getCustomerCredentials(customerId)
        }
    }

    companion object {
        val customerId: UUID = UUID.fromString("1d8e1c1d-61a4-44a0-a41e-c8a487a90618")
        val credentialBody = """
            {
              "credentialId": "94d0aa3e-ca3d-482f-b535-4b02bf7bcc9c",
              "customerId": "$customerId",
              "state": "IN_PROGRESS"
            }
        """.trimIndent()
    }
}
Pact JSON Contract customer-manager-iam-manager.json
{
  "provider": {
    "name": "iam-manager"
  },
  "consumer": {
    "name": "customer-manager"
  },
  "interactions": [
    {
      "description": "A request to get customer credential id by customer id",
      "request": {
        "method": "GET",
        "path": "/credential/by-customer/1d8e1c1d-61a4-44a0-a41e-c8a487a90618"
      },
      "response": {
        "status": 200,
        "headers": {
          "Content-Type": "application/json; charset=UTF-8"
        },
        "body": {
          "credentialId": "94d0aa3e-ca3d-482f-b535-4b02bf7bcc9c",
          "customerId": "1d8e1c1d-61a4-44a0-a41e-c8a487a90618",
          "state": "IN_PROGRESS"
        }
      },
      "providerStates": [
        {
          "name": "credential exist"
        }
      ]
    }
  ],
  "metadata": {
    "pactSpecification": {
      "version": "3.0.0"
    },
    "pact-jvm": {
      "version": "4.0.10"
    }
  }
}
Command to publish contract to pact flow
pact-broker publish build/pacts \
 --consumer-app-version=git_commit_sha \
  --broker-base-url=https://safi.pactflow.io/ \
   --broker-token=xxxxx \
   --branch=main

Provider Pact Test

  • Annotated with @Tag("contract-verification")

  • Provider states allow you to set up data on the provider before the interaction is run, so that it can make a response that matches what the consumer expects.

  • Contract test won’t verify Does the provider do the right thing with the request?

IAM - ContractVerificationTest
package ph.safibank.iammanager

import au.com.dius.pact.provider.junit.Provider
import au.com.dius.pact.provider.junit.State
import au.com.dius.pact.provider.junit.StateChangeAction
import au.com.dius.pact.provider.junit.loader.PactBroker
import au.com.dius.pact.provider.junit.loader.PactBrokerAuth
import au.com.dius.pact.provider.junit5.HttpTestTarget
import au.com.dius.pact.provider.junit5.PactVerificationContext
import au.com.dius.pact.provider.junit5.PactVerificationInvocationContextProvider
import io.micronaut.runtime.server.EmbeddedServer
import io.micronaut.test.annotation.MockBean
import io.micronaut.test.extensions.junit5.annotation.MicronautTest
import io.mockk.every
import io.mockk.mockk
import jakarta.inject.Inject
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.TestTemplate
import org.junit.jupiter.api.extension.ExtendWith
import ph.safibank.iammanager.model.SigningCredentials
import ph.safibank.iammanager.model.SigningCredentials.State.IN_PROGRESS
import ph.safibank.iammanager.service.SigningCredentialsManagerService
import java.util.UUID

@Tag("contract-verification")
@MicronautTest
@Provider("iam-manager")
@PactBroker(authentication = PactBrokerAuth(token = "\${pactbroker.auth.token}"))
class ContractVerificationTest {
    @Inject
    private lateinit var embeddedServer: EmbeddedServer
    @Inject
    lateinit var signingCredentialsManagerServiceMock: SigningCredentialsManagerService

    @MockBean(SigningCredentialsManagerService::class)
    fun signingCredentialsManagerServiceMock(): SigningCredentialsManagerService = mockk()

    @BeforeEach
    fun setup(context: PactVerificationContext) {
        context.target = HttpTestTarget(port = embeddedServer.port)
    }

    @TestTemplate
    @ExtendWith(PactVerificationInvocationContextProvider::class)
    fun pactVerificationTestTemplate(context: PactVerificationContext) {
        context.verifyInteraction()
    }

    @State(value = ["credential exist"], action = StateChangeAction.SETUP)
    fun getCredentialByCustomerId() {
        val customerId = UUID.fromString("1d8e1c1d-61a4-44a0-a41e-c8a487a90618")
        val signCredential =SigningCredentials(
            credentialId = "94d0aa3e-ca3d-482f-b535-4b02bf7bcc9c",
            customerId = customerId,
            state = IN_PROGRESS
        )
        every { signingCredentialsManagerServiceMock.getCredentialsByCustomerId(customerId) } .returns(signCredential)
    }
}
Gradle task - contactVerification
val contactVerification = tasks.register("contactVerification", Test::class) {
    group = "verification"
    useJUnitPlatform {
        includeTags("contract-verification")
    }
    systemProperty("pactbroker.auth.token", System.getProperty("pactbroker.auth.token"))
    systemProperty("pactbroker.host", System.getProperty("pactbroker.host"))
    systemProperty("pactbroker.schema", System.getProperty("pactbroker.schema"))
    systemProperty("pact.provider.version", System.getProperty("pact.provider.version"))
    systemProperty("pact.verifier.publishResults", System.getProperty("pact.verifier.publishResults"))
}
./gradlew contactVerification \
-Dpactbroker.auth.token=xxxxxxx \
-Dpactbroker.host=safi.pactflow.io \
-Dpactbroker.scheme=https \
-Dpact.provider.version=git-commit-sha

Can I Deploy

Before you deploy a new version of a service to a higher environment, you need to know whether or not the version you're about to deploy is compatible with the versions of the other services/apps that already exist in that environment.

Pactflow provides this feature to helps us deploy safely in practice.

To make sure you are safe to deploy an application, add the following line before deploying:

pact-broker can-i-deploy --pacticipant PACTICIPANT --version VERSION --to-environment ENVIRONMENT

and add the following line after deploying:

pact-broker record-deployment --pacticipant PACTICIPANT --version VERSION --environment ENVIRONMENT

UI to check Can I Deploy

References

https://docs.pact.io/

https://pactflow.io/how-pact-works/?utm_source=ossdocs&utm_campaign=getting_started#slide-1

Contract Testing and PACT

Contract Testing

https://docs.pact.io/pact_broker/can_i_deploy