Created by Jideo Pena on Feb 22, 2023
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