Skip to main content
  1. Articles/

Golang Tutorial: Contract Testing with PACT

·2678 words·13 mins· loading · loading ·
Marko Milojevic
Author
Marko Milojevic
Software engineer and architect. Golang and LLM enthusiast. Awful chess player, gym rat, harmonica newbie and cat lover.
Testing in Golang - This article is part of a series.
Part 2: This Article

My favorite part of software development is writing tests, whether they are unit tests or integration tests. I enjoy the process immensely. There’s a certain satisfaction in creating a test case that uncovers a function’s failure. It brings me joy to discover a bug during development, knowing that I’ve fixed it before anyone encounters it in a test environment or, worse, in production. Sometimes, I stay up late just to write more tests; it’s like a hobby. I even spent around 30 minutes on my wedding day writing unit tests for my personal project, but don’t tell my wife!

The only thing that used to bother me was dealing with integration issues between multiple Microservices. How could I ensure that two Microservices, each with specific versions, wouldn’t face integration problems? How could I be certain that a new version of a Microservice didn’t break its API interface, rendering it unusable for others? This information was crucial to have before launching extensive scenarios in our end-to-end testing pipeline. Otherwise, we’d end up waiting for an hour just to receive feedback that we’d broken the JSON schema.

Then, one day in the office, I heard a rumor that we were planning to use Contract Testing. I quickly checked the first article I found, and I was amazed. It was a breakthrough.

Contract Testing
#

There are many excellent articles about Contract testing, but the one I like the most is from Pactflow. Contract testing ensures that two parties can communicate effectively by testing them in isolation to verify if both sides support the messages they exchange. One party, known as the Consumer, captures the communication with the other party, referred to as the Provider, and creates the Contract. This Contract serves as a specification for the expected requests from the Consumer and the responses from the Provider. Application code automatically generates Contracts, typically during the unit testing phase. Automatic creation ensures that each Contract accurately reflects the latest state of affairs.

Contract testing
Contract testing

After the Consumer publishes the Contract, the Provider can use it. In its code, likely within unit tests, the Provider conducts Contract verification and publishes the results. In both phases of Contract testing, we work solely on one side, without any actual interaction with the other party. Essentially, we are ensuring that both parties can communicate with each other within their separate pipelines. As a result, the entire process is asynchronous and independent. If either of these two phases fails, both the Consumer and Provider must collaborate to resolve integration issues. In some cases, the Consumer may need to adapt its integrational code, while in others, the Provider may need to adjust its API.

It’s essential to note that Contract testing is NOT Schema testing. Schema testing is confined to one party without any connection to another. In contrast, Contract testing verifies interactions on both sides and ensures compatibility between desired versions of both parties. Additionally, Contract testing is NOT End-to-End testing. End-to-End testing involves testing a group of services running together, typically evaluating the entire system, from the UI down to storage. In contrast, Contract testing conducts tests against each service independently. These tests are isolated and do not require more than one service to be running simultaneously.

Now, let’s delve deeper into what Contract testing entails.

PACT
#

PACT is a tool designed for Contract testing, and we utilize it to facilitate the validation of communication between Consumers and Providers over the HTTP protocol. It also extends its support to the testing of message queues such as SQS, RabbitMQ, Kafka, and more.

On the consumer side, we create Contracts using the PACT DSL tailored for a specific programming language. These Contracts encompass interactions that define expected requests and their corresponding minimal responses.

During the test execution, the Consumer sends requests to a Mock Provider, which employs the defined interactions to compare actual and expected HTTP requests. When the requests align, the Mock Provider returns the expected minimal response. This allows the Consumer to verify whether it meets the anticipated criteria.

Consumer Testing
Consumer Testing

On the Provider side, we employ the previously created Contracts to ascertain if the server can fulfill the expected requirements. The outcomes of this verification process can be published to maintain a record of which versions of Consumers and Providers are compatible.

During Provider-side testing, a Mock Consumer is responsible for sending the expected request to the Provider. The Provider then assesses whether the incoming HTTP request aligns with the expectations and subsequently generates a response. In the final step, the Mock Consumer compares the actual response with the anticipated minimal response and delivers the result of the verification process.

Provider Testing
Provider Testing

All Contracts and verification results can be stored on the PACT Broker. The PACT Broker is a tool that developers usually need to host and maintain themselves on most projects. Alternatively, a public option like PactFlow is available for use.

Simple server and client in Go
#

To write the first Contract, we need to provide some code for a simple server and client. In this case, the server should have one endpoint, /users/:userId, for returning Users by their ID. The code generates the result to avoid the need for more complex logic, such as communication with a database.

File /pkg/server/server.go

type User struct {
	ID        string `json:"id"`
	FirstName string `json:"firstName"`
	LastName  string `json:"lastName"`
}

func GetUserByID(ctx *gin.Context) {
	id := ctx.Param("userId")

	ctx.JSON(http.StatusOK, User{
		ID:        id,
		FirstName: fmt.Sprintf("first%s", id),
		LastName:  fmt.Sprintf("last%s", id),
	})
}

File /cmd/main.go

func main() {
	router := gin.Default()
	router.GET("/users/:userId", server.GetUserByID)
	router.Run(":8080")
}

The complete code for the server is split into two files: server.go and main.go. For this demonstration, I’ve used the Gin web framework for Go, but any other framework (or even no framework at all) would suffice. The client code is even simpler and consists of two files: client.go and main.go. It creates and sends a GET request to the /users/:userId endpoint. Upon receiving the result, it parses the JSON body into the User struct.

File /pkg/client/client.go

func GetUserByID(host string, id string) (*server.User, error) {
	uri := fmt.Sprintf("http://%s/users/%s", host, id)
	resp, err := http.Get(uri)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	var user server.User
	err = json.NewDecoder(resp.Body).Decode(&user)
	if err != nil {
		return nil, err
	}

	return &user, nil
}

File /cmd/main.go

func main() {
	user, err := client.GetUserByID("localhost:8080", "1")
	if err != nil {
		panic(err)
	}

	fmt.Println(user)
}

In both the client and server code, I have included dedicated functions for handling requests and responses. This code structure is crucial to facilitate the creation of specific test functions for the code we intend to test later.

PACT test for the client
#

Writing a PACT test in Go is similar to writing unit tests. In this case, we should also depend on the [package](https://github.com/pact-foundation/pact-go “package) from the Pact Foundation. Installation is a straightforward process, and it supports Go Modules.

File /pkg/client/client_test.go

import (
	//
	// some imports
	//
	"github.com/pact-foundation/pact-go/types"
	"github.com/pact-foundation/pact-go/dsl"
)

func TestClientPact_Local(t *testing.T) {
	// initialize PACT DSL
	pact := dsl.Pact{
		Consumer: "example-client",
		Provider: "example-server",
	}

	// setup a PACT Mock Server
	pact.Setup(true)

	t.Run("get user by id", func(t *testing.T) {
		id := "1"

		
		pact.
			AddInteraction(). // specify PACT interaction
			Given("User Alice exists"). // specify Provider state
			UponReceiving("User 'Alice' is requested"). // specify test case name
			WithRequest(dsl.Request{ // specify expected request
				Method: "GET", 
				// specify matching for endpoint
				Path:   dsl.Term("/users/1", "/users/[0-9]+"),
			}).
			WillRespondWith(dsl.Response{ // specify minimal expected response
				Status: 200,
				Body: dsl.Like(server.User{ // pecify matching for response body
					ID:        id,
					FirstName: "Alice",
					LastName:  "Doe",
				}),
			})

		// verify interaction on client side
		err := pact.Verify(func() error {
			// specify host anf post of PACT Mock Server as actual server
			host := fmt.Sprintf("%s:%d", pact.Host, pact.Server.Port)
			
			// execute function
			user, err := GetUserByID(host, id)
			if err != nil {
				return errors.New("error is not expected")
			}

			// check if actual user is equal to expected
			if user == nil || user.ID != id {
				return fmt.Errorf("expected user with ID %s but got %v", id, user)
			}

			return err
		})

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

	// write Contract into file
	if err := pact.WritePact(); err != nil {
		t.Fatal(err)
	}

	// stop PACT mock server
	pact.Teardown()
}

As you can see in the example above, I have provided the PACT test in Go as a simple unit test. At the beginning of the test, I have defined a PACT DSL and run the Mock Server. The critical point of the test is defining an Interaction. An Interaction contains the state of the Provider, the name of a test case, the expected request, and the expected minimal response. We can define many attributes for both the request and response, including body, headers, query, status code, etc.

After defining the Interaction, the next step is Verification. The PACT test runs our client, which now sends a request to the PACT Mock Server instead of a real one. I have ensured this by providing the Mock Server’s host to the GetUserByID method. If the actual request matches the expected one, the Mock Server sends back the expected minimal response. Inside the test, we can make a final check if our method returns the correct User after extracting it from the JSON body.

The last step involves writing the Interaction in the form of a Contract. PACT stores the Contract inside the pacts folder by default, but we can change that during PACT DSL initialization. After executing the code, the final output should look like this:

Client testing console output

=== RUN   TestClientPact_Local
2021/08/29 13:55:25 [INFO] checking pact-mock-service within range >= 3.5.0, < 4.0.0
2021/08/29 13:55:26 [INFO] checking pact-provider-verifier within range >= 1.31.0, < 2.0.0
2021/08/29 13:55:26 [INFO] checking pact-broker within range >= 1.22.3
2021/08/29 13:55:27 [INFO] INFO  WEBrick 1.4.2
2021/08/29 13:55:27 [INFO] INFO  ruby 2.6.3 (2019-04-16) [universal.x86_64-darwin19]
2021/08/29 13:55:27 [INFO] INFO  WEBrick::HTTPServer#start: pid=21959 port=56423
--- PASS: TestClientPact_Local (2.31s)
=== RUN   TestClientPact_Local/get_user_by_id
2021/08/29 13:55:27 [INFO] INFO  going to shutdown ...
2021/08/29 13:55:28 [INFO] INFO  WEBrick::HTTPServer#start done.
    --- PASS: TestClientPact_Local/get_user_by_id (0.02s)
PASS

PACT test for the server
#

Writing PACT tests for the server is easier. The idea is to verify the desired Contracts that the client already provides. We also write PACT tests for servers in the form of unit tests.

File /pkg/server/server_test.go

import (
	//
	// some imports
	//
	"github.com/pact-foundation/pact-go/types"
	"github.com/pact-foundation/pact-go/dsl"
)

func TestServerPact_Verification(t *testing.T) {
	// initialize PACT DSL
	pact := dsl.Pact{
		Provider: "example-server",
	}

	// verify Contract on server side
	_, err := pact.VerifyProvider(t, types.VerifyRequest{
		ProviderBaseURL: "http://127.0.0.1:8080",
		PactURLs:        []string{"../client/pacts/example-client-example-server.json"},
	})

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

The client has already provided a Contract as a JSON file that contains all interactions. Here, we also need to define PACT DSL and then execute the verification of the Contract. During the Contract’s verification, PACT Mock Client sends expected requests to the server, specified in one of the Interactions in the Contract. The server receives the request and returns the actual response. Mock Client gets the response and matches it with the expected minimal response. If the whole process of verification is successful, we should get an output similar to this:

Server testing console output

=== RUN   TestServerPact_Verification
2021/08/29 14:41:13 [INFO] checking pact-mock-service within range >= 3.5.0, < 4.0.0
2021/08/29 14:41:14 [INFO] checking pact-provider-verifier within range >= 1.31.0, < 2.0.0
2021/08/29 14:41:14 [INFO] checking pact-broker within range >= 1.22.3
--- PASS: TestServerPact_Verification (2.45s)
=== RUN   TestServerPact_Verification/Pact_between__and__
    --- PASS: TestServerPact_Verification/Pact_between__and__ (0.00s)
=== RUN   TestServerPact_Verification/has_status_code_200
    pact.go:637: Verifying a pact between example-client and example-server Given User Alice exists User 'Alice' is requested with GET /users/1 returns a response which has status code 200
    --- PASS: TestServerPact_Verification/has_status_code_200 (0.00s)
=== RUN   TestServerPact_Verification/has_a_matching_body
    pact.go:637: Verifying a pact between example-client and example-server Given User Alice exists User 'Alice' is requested with GET /users/1 returns a response which has a matching body
    --- PASS: TestServerPact_Verification/has_a_matching_body (0.00s)
PASS

Usage of PACT Broker with PactFlow
#

As mentioned earlier in this article, the usage of PACT testing cannot be completed without the PACT Broker. We do not expect to have access to Contracts in physical files from clients inside the pipelines of our servers. For that purpose, development teams should use a standalone PACT Broker dedicated to that project. It is possible to use the Docker image provided by PACT Foundation and have it installed as part of your infrastructure. Also, if you are willing to pay for a PACT Broker, the solution from PactFlow is perfect, and registration is simple. For this article, I have been using the trial version of PactFlow, which allows me to store up to five Contracts.

PactFlow overview
PactFlow overview

To publish Contracts to PactFlow, I need to make minor adaptations inside the client test. These adaptations include the new part where I have defined a PACT Publisher that uploads all Contracts after the test execution.

Adapted File /pkg/client/client_test.go

func TestClientPact_Broker(t *testing.T) {
	pact := dsl.Pact{
		Consumer: "example-client",
		Provider: "example-server",
	}

	t.Run("get user by id", func(t *testing.T) {
		id := "1"

		pact.
			AddInteraction().
			Given("User Alice exists").
			UponReceiving("User 'Alice' is requested").
			WithRequest(dsl.Request{
				Method: "GET",
				Path:   dsl.Term("/users/1", "/users/[0-9]+"),
			}).
	//
    	// PACT verification
    	//
	})

	if err := pact.WritePact(); err != nil {
		t.Fatal(err)
	}

  	// specify PACT publisher
	publisher := dsl.Publisher{}
	err := publisher.Publish(types.PublishRequest{
	    // a folder with all PACT test
		PactURLs:        []string{"./pacts/"}, 
		// PACT broker URI
		PactBroker:      "<PACT BROKER>", 
		// API token for PACT broker
		BrokerToken:     "<API TOKEN>", 
		ConsumerVersion: "1.0.0",
		Tags:            []string{"1.0.0", "latest"},
	})
	if err != nil {
		t.Fatal(err)
	}

	pact.Teardown()
}

After registering with PactFlow, you should receive the new host for your PACT Broker. Additionally, you should use your API token to complete the Publisher definition in the code. You can find API tokens within the dashboard overview settings. When we execute the new client test, it adds the first Contract to PactFlow. This Contract has tags 1.0.0, latest, and master (which are added by default).

PactFlow first contract
PactFlow first contract

To create a difference in the client, I adapted the test to send a request to the other endpoint /user/:userId instead of /users/:userId. Additionally, I changed the Tag and ConsumerVersion to be 1.0.1 instead of 1.0.0. After executing the test, the additional Contract will appear.

PactFlow second contract
PactFlow second contract

Next, I adapted the server test to accommodate the changes. The new adjustments are made in the Verification process. It now includes the PACT Broker host, API token, and a decision to publish the verification result.

Adapted File /pkg/server/server_test.go

func TestServerPact_BrokerVerification(t *testing.T) {
	pact := dsl.Pact{
		Provider: "example-server",
	}

	_, err := pact.VerifyProvider(t, types.VerifyRequest{
		BrokerURL:                  "<PACT BROKER>",
		BrokerToken:                "<API TOKEN>",
		ProviderBaseURL:            "http://127.0.0.1:8080",
		ProviderVersion:            "1.0.0",
		ConsumerVersionSelectors: []types.ConsumerVersionSelector{
			{
				Consumer: "example-client",
			 	Tag:      "1.0.0",
			},
		},
		PublishVerificationResults: true, // publish results of verification to PACT broker
	})

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

In addition, it should also include a selector for the Consumer name and version to perform verification with the correct Contract version. The first execution, which passed successfully, checks the client version 1.0.0. However, the second execution of the test, which failed, is for checking the client version 1.0.1. The second execution is expected to fail because the server still listens to the /users/:userId endpoint.

PactFlow first verification
PactFlow first verification

To fix the integration between the newest client and server, we need to make adjustments to either of them. In this case, I have decided to modify the server to listen to the new /user/:usersId endpoint. After updating the server to the new version 1.0.1 and executing PACT verification once again, the test passes successfully, and it publishes new verification results on the PACT Broker.

PactFlow second verification
PactFlow second verification

On PactFlow, as well as on any other PACT Broker, we can review the history of each Contract’s verification process by accessing a specific Contract from the dashboard overview and then examining its Matrix tab.

PactFlow matrix
PactFlow matrix

Conclusion
#

Writing PACT tests is a fast and cost-effective way to incorporate into our pipeline. By validating Consumers and Providers early in our CI/CD process, we save time and receive early feedback on our integration outcomes. Contract tests enable us to utilize the current versions of our clients and servers, evaluating them independently to determine their compatibility.

Useful Resources
#

Testing in Golang - This article is part of a series.
Part 2: This Article

Related

Golang Tutorial: Unit Testing with Mocking

·3513 words·17 mins· loading · loading
Unit testing has always been my thing, almost like a hobby. There was a time when I was obsessed with it, and I made sure that all my projects had at least 90% unit test coverage. You can probably imagine how much time it can take to make such a significant change in the codebase. However, the result was worth it because I rarely encountered bugs related to business logic. Most of the issues were related to integration problems with other services or databases. Adding new business rules was a breeze because there were already tests in place to cover all the cases from before. The key was to ensure that these tests remained successful in the end. Sometimes, I didn’t even need to check the entire running service; having the new and old unit tests pass was sufficient. Once, while working on a personal project, I had to write unit tests to cover numerous Go structs and functions—more than 100 in total. It consumed my entire weekend, and late on a Sunday night, before heading out on a business trip the next day, I set an alarm clock to wake me up. I had hardly slept that night; it was one of those restless nights when you dream but are also aware of yourself and your surroundings. My brain was active the entire time, and in my dreams, I kept writing unit tests for my alarm clock. To my surprise, each time I executed a unit test in my dream, the alarm rang. It continued ringing throughout the night. And yes, I almost forgot to mention, for two years, we had zero bugs in production. The application continued to fetch all the data and send all the emails every Monday. I don’t even remember my Gitlab password anymore. Unit Testing and Mocking (in general) # In Martin Fowler’s article, we can identify two types of unit tests: Sociable unit tests, where we test a unit while it relies on other objects in conjunction with it. For example, if we want to test the UserController, we would test it along with the UserRepository, which communicates with the database. Solitary unit tests, where we test a unit in complete isolation. In this scenario, we would test the UserController, which interacts with a controlled, mocked UserRepository. With mocking, we can specify how it behaves without involving a database.

Golang Tutorial: Generics with Gorm

·1741 words·9 mins· loading · loading
After months and years of talking, trying things out, and testing, we’ve finally reached a big moment in our favorite programming language. The new Golang version, 1.18, is here. We knew it would bring significant changes to Go’s codebase, even before Generics was officially released. For a long time, when we wanted to make our code more general and abstract, we used code generators in Go. Learning what the “Go way” of doing things was challenging for many of us, but it also led to many breakthroughs. It was worth the effort. Now, there are new possibilities on the horizon. Many new packages have emerged, giving us ideas on how we can improve the Go ecosystem with reusable code that makes life easier for all of us. This inspiration led me to create a small proof of concept using the Gorm library. Now, let’s take a look at it. Source code # When I wrote this article, it relied on a GitHub Repository. The code served as a Go library proof of concept, with my intention to continue working on it. However, it was not yet suitable for production use, and I had no plans to offer production support at that time. You can find the current features by following the link, and below, there is a smaller sample snippet. Example Usage package main import ( "github.com/ompluscator/gorm-generics" // some imports ) // Product is a domain entity type Product struct { // some fields } // ProductGorm is DTO used to map Product entity to database type ProductGorm struct { // some fields } // ToEntity respects the gorm_generics.GormModel interface func (g ProductGorm) ToEntity() Product { return Product{ // some fields } } // FromEntity respects the gorm_generics.GormModel interface func (g ProductGorm) FromEntity(product Product) interface{} { return ProductGorm{ // some fields } } func main() { db, err := gorm.Open(/* DB connection string */) // handle error err = db.AutoMigrate(ProductGorm{}) // handle error // initialize a new Repository with by providing // GORM model and Entity as type repository := gorm_generics.NewRepository[ProductGorm, Product](db) ctx := context.Background() // create new Entity product := Product{ // some fields } // send new Entity to Repository for storing err = repository.Insert(ctx, &product) // handle error fmt.Println(product) // Out: // {1 product1 100 true} single, err := repository.FindByID(ctx, product.ID) // handle error fmt.Println(single) // Out: // {1 product1 100 true} } Why have I picked ORM for PoC? # Coming from a background in software development with traditional object-oriented programming languages like Java, C#, and PHP, one of the first things I did was search Google for a suitable ORM for Golang. Please forgive my inexperience at the time, but that’s what I was expecting. It’s not that I can’t work without an ORM. It’s just that I don’t particularly like how raw MySQL queries appear in the code. All that string concatenation looks messy to me. On the other hand, I always prefer to dive right into writing business logic, with minimal time spent thinking about the underlying data storage. Sometimes, during the implementation, I change my mind and switch to different types of storage. That’s where ORMs come in handy.

Golang Tutorial: Generics

·3344 words·16 mins· loading · loading
How often do we encounter significant changes in our preferred programming language? Some languages undergo frequent updates, while others remain traditional and stable. Go falls into the latter category, known for its consistency. “This is not the Go way!” is a phrase that often comes to mind. Most Go releases have focused on refining its existing principles. However, a major shift is on the horizon. The Go team has announced that Generics in Go are becoming a reality, moving beyond mere discussion and into implementation. Brace yourselves, a revolution is coming. What are Generics? # Generics allow us to parameterize types when defining interfaces, functions, and structs. Generics is not a new concept. It has been used since the first version of Ada, through templates in C++, to its modern implementations in Java and C#. To illustrate without delving into complex definitions, let’s examine a practical example. Instead of having multiple Max or Min functions like this: Without Generics func MaxInt(a, b int) int { // some code } func MaxFloat64(a, b float64) float64 { // some code } func MaxByte(a, b byte) byte { // some code } we can declare now only one method, like this: With Generics func Max[T constraints.Ordered](a, b T) T { // some code } Wait, what just happened? Instead of defining a method for each type in Go, we utilized Generics. We used a generic type, parameter T, as an argument for the method. With this minor adjustment, we can support all orderable types. The parameter T can represent any type that satisfies the Ordered constraint (we will discuss constraints later). Initially, we need to specify what kind of type T is. Next, we determine where we want to use this parameterized type. In this case, we’ve specified that both input arguments and the output should be of type T. If we execute the method by defining T as an integer, then everything here will be an integer: Execute Generic Function func main() { fmt.Println(Max[int](1, 2)) } // // this code behaves exactly like method: // Max(a, b int) int And it doesn’t stop there. We can provide as many parameterized types as we need and assign them to different input and output arguments as desired: Execute some complex Generic Function func Do[R any, S any, T any](a R, b S) T { // some code } func main() { fmt.Println(Do[int, uint, float64](1, 2)) } // // this code behaves exactly like method: // Do(a int, b uint) float64 Here we have three parameters: R, S, and T. As we can see from the any constraint (which behaves like interface{}), those types can be, well, anything. So, up to this point, we should have a clear understanding of what generics are and how we use them in Go. Let’s now focus on more exciting consequences.