Writing Pact Contract Tests with GoLang

Ahmet Taskin
Trendyol Tech
Published in
6 min readAug 4, 2020

Introduction

In Trendyol, we are using microservice architecture. So, there are many teams developing their own microservices in different languages like JAVA, GoLang etc. There many integrations between these microservices on live systems. For example, one of the sevice developed by my team (Trendyol instant messaging or chat) gets order information of a user from another service developed by order management system (OMS) team. If OMS team changes the response object without informing us, probably our service would be broken in production environment. To prevent incidents like this, and to deploy the changes to the production environment without breaking anything, we are writing Pact Contract Tests on both consumer and provider side. So, if one team changes the contract without informing the other team, their service would fail during the build since it cannot verify the contract on Pact Broker. So, if there is a need to change the contract, first the consumer service should be updated and the new generated pact file should be uploaded to Pact Broker, then provider can be updated and verify the new contract. Thats why it is also called consumer-driven contract testing. In this article, i am gonna demonstrate an example for both consumer and provider tests with GoLang including Pact Broker verification . More info about Pact contract testing framework can be found here.

For the scenario in this article, there is a consumer called “chat-gateway-api”, and there is a provider called “chat-api”. In this scenario, chat-gateway-api gets live chat status from chat-api. According to the response, chat-gateway-api either enables live chat connection link to the customer for connecting customers to agents or shows the information message to the customer for informing about working hours of agents.

Consumer and Provider Diagram

Consumer Test

For consumer pact tests, mock provider is configured and then consumer requests and expected provider responses are defined. While running the test, consumer requests are handled by the mock provider created by pact framework instead of real provider service. Mock provider returns expected response according the configuration in the test. Consumer test confirms the response, and then publishes the auto-generated pact json file to a pact broker. The auto-generated pact json file includes both consumer responses and related provider responses. A basic diagram for consumer tests can be seen below;

Consumer Test Diagram

Step 1- Preparing pact mock http server

First, we need to setup pact mock server because consumer requests will be handled by pact mock provider during the consumer test. Also, we need define the consumer name, provider name, a directory to put auto-generated pact file, and a directory to put logs. A basic pact consumer configuration can be like this;

func setup() {
pact = dsl.Pact{
Consumer: "chat-gateway-api",
Provider: "chat-api",
LogDir: "../../logs",
PactDir: "../../pacts",
LogLevel: "INFO",
DisableToolValidityCheck: true,
}

pact.Setup(true)

fakeLogger := &testfiles.FakeLogger{}
fakeLogger.On("Error")

httpClient := resty.New()
baseConfig := models.ApiConfigBase{
Url: fmt.Sprintf("http://localhost:%d", pact.Server.Port),
SleepWindow: 1000,
RequestVolumeThreshold: 5,
ErrorPercentThreshold: 5,
MaxConcurrentRequests: 100,
Timeout: 3,
}
apiConfig := models.ApiConfig{
ChatApi: baseConfig,
}
client = clients.NewChatApiClient(fakeLogger, httpClient, apiConfig)
}

Step 2 — Writing pact consumer contract test

After defining mock provider with pact framework, requests and responses must be defined. In the code below, requests to “api/chatrule/livechatstatus” path would be responded as defined below.

t.Run("Get live chat status", func(t *testing.T) {
orderResponse := `{
"success":true,
"errormessages":null,
"data":true
}`
pact.
AddInteraction().
Given("Get live chat status").
UponReceiving("A request to get the last order of the user.").
WithRequest(request{
Method: "GET",
Path: term("/api/chatrule/livechatstatus", "/api/chatrule/livechatstatus"),
Headers: headersWithTokenForChatStatus,
}).
WillRespondWith(dsl.Response{
Status: 200,
Body: orderResponse,
Headers: commonHeaders,
})

err := pact.Verify(func() error {
liveChatStatus, err := client.GetChatStatus("segment-id", "normal", "correlation-id", nil)

// Assert basic fact
if liveChatStatus != true {
return fmt.Errorf("wanted chat status true but got false")
}
return err
})

if err != nil {
t.Fatalf("Error on Verify: %v", err)
}
})

publish()

After running the test above, a json file is created by the pact framework under the path we defined in the configuration. The name of the auto generated pact file is {consumer-name}-{provider-name}.json. It includes requests and expected responses under interactions.

{
"consumer": {
"name": "chat-gateway-api"
},
"provider": {
"name": "chat-api"
},
"interactions": [
{
"description": "A request to get the last order of the user.",
"providerState": "Get live chat status",
"request": {
"method": "GET",
"path": "/api/chatrule/livechatstatus",
"headers": {
"Content-Type": "application/json",
"SegmentId": "segment-id",
"X-Correlation-Id": "corrId",
"x-delivery-type": "normal"
},
"matchingRules": {
"$.path": {
"match": "regex",
"regex": "\\/api\\/chatrule\\/livechatstatus"
},
"$.headers.Content-Type": {
"match": "type"
},
"$.headers.SegmentId": {
"match": "type"
},
"$.headers.X-Correlation-Id": {
"match": "type"
},
"$.headers.x-delivery-type": {
"match": "type"
}
}
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json; charset=utf-8"
},
"body": {
"success": true,
"errormessages": null,
"data": true
},
"matchingRules": {
"$.headers.Content-Type": {
"match": "regex",
"regex": "application\\/json"
}
}
}
}
],
"metadata": {
"pactSpecification": {
"version": "2.0.0"
}
}
}

Step 3 — Publishing pact file to pact broker

To run a local pact broker , this docker-compose file can be used.

To publish the pact json file to the broker, code block below can be used;

version := "1.0.0"

// Publish the Pacts...
p := dsl.Publisher{}

fmt.Println("Publishing Pact files to broker", os.Getenv("PACT_DIR"), os.Getenv("PACT_BROKER_URL"))
err := p.Publish(types.PublishRequest{
PactURLs: []string{filepath.FromSlash("../../pacts/chat-gateway-api-chat-api.json")},
PactBroker: "http://localhost",
ConsumerVersion: version,
Tags: []string{"master"},
BrokerUsername: "",
BrokerPassword: "",
})

if err != nil {
fmt.Println("ERROR: ", err)
os.Exit(1)

After publishing auto-generated pact file, it can be seen in the pact broker. As seen below, it is published to the broker, but it is not verified by the provider yet. Thats why last verified column is empty.

Provider Test

Provider tests are entirely driven by the pact framework. First pact framework gets the proper pact file from the pact broker. Then, it reads the pact file and make the requests defined in the pact file to the provider and receive responses for each request. Then, it compares the responses received from the actual provider against to responses defined in the pact file. If everything is okey, it verifies the pact file on the broker, otherwise verification fails. A sample diagram for the provider tests can be seen below;

Provider Test Diagram

A sample code for provider tests as simple as below;

pact := dsl.Pact{
Provider: "chat-api",
LogDir: "../../logs",
PactDir: "../../pacts",
DisableToolValidityCheck: true,
LogLevel: "INFO",
}

_, err := pact.VerifyProvider(t, types.VerifyRequest{
ProviderBaseURL: fmt.Sprintf("http://127.0.0.1:%d", port),
Tags: []string{"master"},
FailIfNoPactsFound: false,
BrokerURL: "http://localhost",
BrokerUsername: "",
BrokerPassword: "",
PublishVerificationResults: true,
ProviderVersion: "1.0.0",
StateHandlers: stateHandlers,
//RequestFilter: fixBearerToken,
})

if err != nil {
t.Fatal(err)
}

As seen above pact broker, our provider verified the pact file created by the consumer. So, after integrating the pact tests to the build pipeline, it will guarantee that provider and consumer verifies the contract and works together properly

References

https://github.com/pact-foundation/pact-workshop-go

Thanks for reading ..

--

--