Skip to content

pact-foundation/pact-workshop-go

 
 

Repository files navigation

Pact Go workshop

Introduction

This workshop is aimed at demonstrating core features and benefits of contract testing with Pact.

Whilst contract testing can be applied retrospectively to systems, we will follow the consumer driven contracts approach in this workshop - where a new consumer and provider are created in parallel to evolve a service over time, especially where there is some uncertainty with what is to be built.

This workshop should take from 1 to 2 hours, depending on how deep you want to go into each topic.

Workshop outline:

NOTE: Each step is tied to, and must be run within, a git branch, allowing you to progress through each stage incrementally. For example, to move to step 2 run the following: git checkout step2

Learning objectives

If running this as a team workshop format, you may want to take a look through the learning objectives.

Scenario

There are two components in scope for our workshop.

  1. Admin Service (Consumer). Does Admin-y things, and often needs to communicate to the User service. But really, it's just a placeholder for a more useful consumer (e.g. a website or another microservice) - it doesn't do much!
  2. User Service (Provider). Provides useful things about a user, such as listing all users and getting the details of individuals.

For the purposes of this workshop, we won't implement any functionality of the Admin Service, except the bits that require User information.

Project Structure

The key packages are shown below:

├── consumer	  # Contains the Admin Service Team (client) project
├── model         # Shared domain model
├── pact          # The directory of the Pact Standalone CLI
├── provider      # The User Service Team (provider) project

Step 1 - Simple Consumer calling Provider

We need to first create an HTTP client to make the calls to our provider service:

Simple Consumer

NOTE: even if the API client had been been graciously provided for us by our Provider Team, it doesn't mean that we shouldn't write contract tests - because the version of the client we have may not always be in sync with the deployed API - and also because we will write tests on the output appropriate to our specific needs.

This User Service expects a users path parameter, and then returns some simple json back:

Sequence Diagram

You can see the client public interface we created in the consumer/client package:

type Client struct {
	BaseURL    *url.URL
	httpClient *http.Client
}

// GetUser gets a single user from the API
func (c *Client) GetUser(id int) (*model.User, error) {
}

We can run the client with make run-consumer - it should fail with an error, because the Provider is not running.

Move on to step 2: Write a unit test for our consumer

Step 2 - Client Tested but integration fails

Now lets create a basic test for our API client. We're going to check 2 things:

  1. That our client code hit the expected endpoint
  2. That the response is marshalled into a User object, with the correct ID

consumer/client/client_test.go

func TestClientUnit_GetUser(t *testing.T) {
	userID := 10

	// Setup mock server
	server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
		assert.Equal(t, req.URL.String(), fmt.Sprintf("/users/%d", userID))
		user, _ := json.Marshal(model.User{
			FirstName: "Sally",
			LastName:  "McDougall",
			ID:        userID,
			Type:      "admin",
			Username:  "smcdougall",
		})
		rw.Write([]byte(user))
	}))
	defer server.Close()

	// Setup client
	u, _ := url.Parse(server.URL)
	client := &Client{
		BaseURL: u,
	}
	user, err := client.GetUser(userID)
	assert.NoError(t, err)

	// Assert basic fact
	assert.Equal(t, user.ID, userID)
}

Unit Test With Mocked Response

Let's run this spec and see it all pass:

$ make unit

--- 🔨Running Unit tests
go test -count=1 github.com/pact-foundation/pact-workshop-go/consumer/client -run 'TestClientUnit'
ok  	github.com/pact-foundation/pact-workshop-go/consumer/client	10.196s

Meanwhile, our provider team has started building out their API in parallel. Let's run our client against our provider (you'll need two terminals to do this):

# Terminal 1
$ make run-provider

2019/10/28 18:24:37 API starting: port 8080 ([::]:8080)

# Terminal 2
make run-consumer

2019/10/28 18:25:57 api unavailable
exit status 1
make: *** [run-consumer] Error 1

Doh! The Provider doesn't know about /users/:id. On closer inspection, the provider only knows about /user/:id and /users.

We need to have a conversation about what the endpoint should be, but first...

Move on to step 3

Step 3 - Pact to the rescue

Let's add Pact to the project. It comes in two parts.

  • Installing pact-go cli
    • Required to install pact-go system libraries
  • Adding pact-go as a dev dependency to your project.

Always check the installation instructions in the docs for your platform.

The following command will install the pact-go CLI tool.

$ go install github.com/pact-foundation/pact-go/v2

and we will use the pact-go CLI tool to install system libraries required by pact-go

$ pact-go -l DEBUG install

You can use the provided make command to do this for you.

$ make install

You can add pact-go to your project with the following

$ go get github.com/pact-foundation/pact-go/v2

We can now write a consumer pact test for the GET /users/:id endpoint.

Note how similar it looks to our unit test:

consumer/client/client_pact_test.go:

	t.Run("the user exists", func(t *testing.T) {
		id := 10

		err = mockProvider.
			AddInteraction().
			Given("User sally exists").
			UponReceiving("A request to login with user 'sally'").
			WithRequestPathMatcher("GET", Regex("/users/"+strconv.Itoa(id), "/users/[0-9]+")).
			WillRespondWith(200, func(b *consumer.V2ResponseBuilder) {
				b.BodyMatch(model.User{}).
					Header("Content-Type", Term("application/json", `application\/json`)).
					Header("X-Api-Correlation-Id", Like("100"))
			}).
			ExecuteTest(t, func(config consumer.MockServerConfig) error {
				// Act: test our API client behaves correctly

				// Get the Pact mock server URL
				u, _ = url.Parse("http://" + config.Host + ":" + strconv.Itoa(config.Port))

				// Initialise the API client and point it at the Pact mock server
				client = &Client{
					BaseURL: u,
				}

				// // Execute the API client
				user, err := client.GetUser(id)

				// // Assert basic fact
				if user.ID != id {
					return fmt.Errorf("wanted user with ID %d but got %d", id, user.ID)
				}

				return err
			})

		assert.NoError(t, err)

	})

Test using Pact

This test starts a Pact mock server on a random port that acts as our provider service. . We can access the update the config.Host & config.Port from consumer.MockServerConfig in the ExecuteTest block and pass these into the Client that we create, after initialising Pact. Pact will ensure our client makes the request stated in the interaction.

Running this test still passes, but it creates a pact file which we can use to validate our assumptions on the provider side, and have conversation around.

$ make consumer

A pact file should have been generated in pacts/GoAdminService-GoUserService.json

Move on to step 4