Writing Pact Contract Tests with GoLang

ahmet taşkın
Aug 4, 2020 · 6 min read

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.

Image for post
Image for post
Consumer and Provider Diagram

Consumer Test

Image for post
Image for post
Consumer Test Diagram

Step 1- Preparing pact mock http server

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

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 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.

Image for post
Image for post

Provider Test

Image for post
Image for post
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)
}
Image for post
Image for post

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 ..

Trendyol Tech

Trendyol Tech Team

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store