Skip to content

Commit

Permalink
Merge pull request #15 from YOU54F/step4
Browse files Browse the repository at this point in the history
chore(deps): Step-4 - Pact V2 Update
  • Loading branch information
YOU54F authored Feb 2, 2024
2 parents 3c1a82f + 6613b25 commit be57b0a
Show file tree
Hide file tree
Showing 115 changed files with 1,091 additions and 24,654 deletions.
33 changes: 33 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: Pact-Workshop(GoLang)

on:
push:
branches:
- step4
pull_request:
branches:
- step4

jobs:
build:
runs-on: ubuntu-latest
defaults:
run:
shell: bash
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.21
- name: install
run: make install
- name: install_pact_ffi_lib
run: make install_pact_ffi_lib
- name: consumer unit tests
run: make unit
- name: consumer pact tests
run: make consumer
- name: provider pact tests
run: make provider || true
17 changes: 11 additions & 6 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
TEST?=./...

include ./make/config.mk

install:
@if [ ! -d pact/bin ]; then\
echo "--- 🛠 Installing Pact CLI dependencies";\
echo "--- Installing Pact CLI dependencies";\
curl -fsSL https://raw.githubusercontent.com/pact-foundation/pact-ruby-standalone/master/install.sh | bash;\
fi

install_pact_ffi_lib:
go install github.com/pact-foundation/pact-go/[email protected]
sudo mkdir -p /usr/local/lib/
sudo $$HOME/go/bin/pact-go -l DEBUG install

run-consumer:
@go run consumer/client/cmd/main.go

Expand All @@ -16,14 +19,16 @@ run-provider:

unit:
@echo "--- 🔨Running Unit tests "
go test -tags=unit -count=1 github.com/pact-foundation/pact-workshop-go/consumer/client -run 'TestClientUnit'
go test -tags=unit -count=1 github.com/pact-foundation/pact-workshop-go/consumer/client -run 'TestClientUnit' -v

consumer: export PACT_TEST := true
consumer: install
@echo "--- 🔨Running Consumer Pact tests "
go test -tags=integration -count=1 github.com/pact-foundation/pact-workshop-go/consumer/client -run 'TestClientPact'
go test -tags=integration -count=1 github.com/pact-foundation/pact-workshop-go/consumer/client -run 'TestClientPact' -v

provider: export PACT_TEST := true
provider: install
@echo "--- 🔨Running Provider Pact tests "
go test -count=1 -tags=integration github.com/pact-foundation/pact-workshop-go/provider -run "TestPactProvider"
go test -count=1 -tags=integration github.com/pact-foundation/pact-workshop-go/provider -run "TestPactProvider" -v

.PHONY: install unit consumer provider run-provider run-consumer
61 changes: 34 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Pact Go workshop

## Introduction
This workshop is aimed at demonstrating core features and benefits of contract testing with Pact. It uses a simple example
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](https://martinfowler.com/articles/consumerDrivenContracts.html) 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.

Expand Down Expand Up @@ -36,11 +36,12 @@ There are two components in scope for our workshop.

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:

```sh
├── consumer # Contains the Admin Service Team (client) project
├── 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
Expand All @@ -54,7 +55,7 @@ We need to first create an HTTP client to make the calls to our provider service

*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 `user` path parameter, and then returns some simple json back:
This User Service expects a `users` path parameter, and then returns some simple json back:

![Sequence Diagram](diagrams/workshop_step1_class-sequence-diagram.png)

Expand Down Expand Up @@ -91,7 +92,7 @@ func TestClientUnit_GetUser(t *testing.T) {

// Setup mock server
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
assert.Equal(t, req.URL.String(), fmt.Sprintf("/user/%d", userID))
assert.Equal(t, req.URL.String(), fmt.Sprintf("/users/%d", userID))
user, _ := json.Marshal(model.User{
FirstName: "Sally",
LastName: "McDougall",
Expand Down Expand Up @@ -163,50 +164,56 @@ Let us add Pact to the project and write a consumer pact test for the `GET /user
t.Run("the user exists", func(t *testing.T) {
id := 10

pact.
err = mockProvider.
AddInteraction().
Given("User sally exists").
UponReceiving("A request to login with user 'sally'").
WithRequest(request{
Method: "GET",
Path: term("/users/10", "/user/[0-9]+"),
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"))
}).
WillRespondWith(dsl.Response{
Status: 200,
Body: dsl.Match(model.User{}),
Headers: commonHeaders,
})
ExecuteTest(t, func(config consumer.MockServerConfig) error {
// Act: test our API client behaves correctly

err := pact.Verify(func() error {
user, err := client.GetUser(id)
// Get the Pact mock server URL
u, _ = url.Parse("http://" + config.Host + ":" + strconv.Itoa(config.Port))

// Assert basic fact
if user.ID != id {
return fmt.Errorf("wanted user with ID %d but got %d", id, user.ID)
}
// Initialise the API client and point it at the Pact mock server
client = &Client{
BaseURL: u,
}

return err
})
// // 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)

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


![Test using Pact](diagrams/workshop_step3_pact.png)


This test starts a mock server a random port that acts as our provider service. To get this to work we update the URL in the `Client` that we create, after initialising 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.

```console
$ make consumer
```

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

*Move on to [step 4](//github.com/pact-foundation/pact-workshop-go/tree/step4)*

Expand Down Expand Up @@ -259,4 +266,4 @@ go test -count=1 -tags=integration github.com/pact-foundation/pact-workshop-go/p

The test has failed, as the expected path `/users/:id` is actually triggering the `/users` endpoint (which we don't need), and returning a _list_ of Users instead of a _single_ User. We incorrectly believed our provider was following a RESTful design, but the authors were too lazy to implement a better routing solution 🤷🏻‍♂️.

*Move on to [step 5](//github.com/pact-foundation/pact-workshop-go/tree/step5)*
*Move on to [step 5](//github.com/pact-foundation/pact-workshop-go/tree/step5)*
128 changes: 55 additions & 73 deletions consumer/client/client_pact_test.go
Original file line number Diff line number Diff line change
@@ -1,108 +1,90 @@
// +build integration
//go:build integration

package client

import (
"fmt"
"os"
"strconv"
"testing"

"net/url"

"github.com/pact-foundation/pact-go/dsl"
"github.com/pact-foundation/pact-go/v2/consumer"
"github.com/pact-foundation/pact-go/v2/log"
"github.com/pact-foundation/pact-go/v2/matchers"
"github.com/pact-foundation/pact-workshop-go/model"
"github.com/stretchr/testify/assert"
)

var commonHeaders = dsl.MapMatcher{
"Content-Type": term("application/json; charset=utf-8", `application\/json`),
"X-Api-Correlation-Id": dsl.Like("100"),
}
var Like = matchers.Like
var EachLike = matchers.EachLike
var Term = matchers.Term
var Regex = matchers.Regex
var HexValue = matchers.HexValue
var Identifier = matchers.Identifier
var IPAddress = matchers.IPAddress
var IPv6Address = matchers.IPv6Address
var Timestamp = matchers.Timestamp
var Date = matchers.Date
var Time = matchers.Time
var UUID = matchers.UUID
var ArrayMinLike = matchers.ArrayMinLike

type S = matchers.S
type Map = matchers.MapMatcher

var u *url.URL
var client *Client

func TestMain(m *testing.M) {
var exitCode int

// Setup Pact and related test stuff
setup()

// Run all the tests
exitCode = m.Run()

// Shutdown the Mock Service and Write pact files to disk
if err := pact.WritePact(); err != nil {
fmt.Println(err)
os.Exit(1)
}
func TestClientPact_GetUser(t *testing.T) {

pact.Teardown()
os.Exit(exitCode)
}
log.SetLogLevel("INFO")
mockProvider, err := consumer.NewV2Pact(consumer.MockHTTPProviderConfig{
Consumer: os.Getenv("CONSUMER_NAME"),
Provider: os.Getenv("PROVIDER_NAME"),
LogDir: os.Getenv("LOG_DIR"),
PactDir: os.Getenv("PACT_DIR"),
})
assert.NoError(t, err)

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

pact.
err = mockProvider.
AddInteraction().
Given("User sally exists").
UponReceiving("A request to login with user 'sally'").
WithRequest(request{
Method: "GET",
Path: term("/users/10", "/users/[0-9]+"),
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"))
}).
WillRespondWith(dsl.Response{
Status: 200,
Body: dsl.Match(model.User{}),
Headers: commonHeaders,
})
ExecuteTest(t, func(config consumer.MockServerConfig) error {
// Act: test our API client behaves correctly

err := pact.Verify(func() error {
user, err := client.GetUser(id)
// Get the Pact mock server URL
u, _ = url.Parse("http://" + config.Host + ":" + strconv.Itoa(config.Port))

// Assert basic fact
if user.ID != id {
return fmt.Errorf("wanted user with ID %d but got %d", id, user.ID)
}
// Initialise the API client and point it at the Pact mock server
client = &Client{
BaseURL: u,
}

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

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

// Common test data
var pact dsl.Pact

// Aliases
var term = dsl.Term

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

func setup() {
pact = createPact()

// Proactively start service to get access to the port
pact.Setup(true)

u, _ = url.Parse(fmt.Sprintf("http://localhost:%d", pact.Server.Port))
return err
})

client = &Client{
BaseURL: u,
}
assert.NoError(t, err)

}
})

func createPact() dsl.Pact {
return dsl.Pact{
Consumer: os.Getenv("CONSUMER_NAME"),
Provider: os.Getenv("PROVIDER_NAME"),
LogDir: os.Getenv("LOG_DIR"),
PactDir: os.Getenv("PACT_DIR"),
LogLevel: "INFO",
}
}
3 changes: 0 additions & 3 deletions consumer/client/cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,10 @@ package main
import (
"log"
"net/url"
"time"

"github.com/pact-foundation/pact-workshop-go/consumer/client"
)

var token = time.Now().Format("2006-01-02")

func main() {
u, _ := url.Parse("http://localhost:8080")
client := &client.Client{
Expand Down
Loading

0 comments on commit be57b0a

Please sign in to comment.