Skip to content

Commit

Permalink
feat(matching): add auto-match capability (#78)
Browse files Browse the repository at this point in the history
  • Loading branch information
Alex Ramey authored and mefellows committed May 17, 2018
1 parent 7b3fb79 commit 16a9146
Show file tree
Hide file tree
Showing 3 changed files with 565 additions and 38 deletions.
120 changes: 82 additions & 38 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,43 +34,43 @@ including [flexible matching](http://docs.pact.io/documentation/matching.html).

<!-- TOC -->

- [Pact Go](#pact-go)
- [Introduction](#introduction)
- [Table of Contents](#table-of-contents)
- [Installation](#installation)
- [Installation on \*nix](#installation-on-\nix)
- [Using Pact](#using-pact)
- [HTTP API Testing](#http-api-testing)
- [Consumer Side Testing](#consumer-side-testing)
- [Provider API Testing](#provider-api-testing)
- [Provider Verification](#provider-verification)
- [API with Authorization](#api-with-authorization)
- [Publishing pacts to a Pact Broker and Tagging Pacts](#publishing-pacts-to-a-pact-broker-and-tagging-pacts)
- [Publishing from Go code](#publishing-from-go-code)
- [Publishing Provider Verification Results to a Pact Broker](#publishing-provider-verification-results-to-a-pact-broker)
- [Publishing from the CLI](#publishing-from-the-cli)
- [Using the Pact Broker with Basic authentication](#using-the-pact-broker-with-basic-authentication)
- [Asynchronous API Testing](#asynchronous-api-testing)
- [Consumer](#consumer)
- [Provider (Producer)](#provider-producer)
- [Pact Broker Integration](#pact-broker-integration)
- [Matching](#matching)
- [Matching on types](#matching-on-types)
- [Matching on arrays](#matching-on-arrays)
- [Matching by regular expression](#matching-by-regular-expression)
- [Match common formats](#match-common-formats)
- [Examples](#examples)
- [HTTP APIs](#http-apis)
- [Asynchronous APIs](#asynchronous-apis)
- [Integrated examples](#integrated-examples)
- [Troubleshooting](#troubleshooting)
- [Splitting tests across multiple files](#splitting-tests-across-multiple-files)
- [Output Logging](#output-logging)
- [Contact](#contact)
- [Documentation](#documentation)
- [Troubleshooting](#troubleshooting-1)
- [Roadmap](#roadmap)
- [Contributing](#contributing)
* [Introduction](#introduction)
* [Table of Contents](#table-of-contents)
* [Installation](#installation)
* [Installation on \*nix](#installation-on-\nix)
* [Using Pact](#using-pact)
* [HTTP API Testing](#http-api-testing)
* [Consumer Side Testing](#consumer-side-testing)
* [Provider API Testing](#provider-api-testing)
* [Provider Verification](#provider-verification)
* [API with Authorization](#api-with-authorization)
* [Publishing pacts to a Pact Broker and Tagging Pacts](#publishing-pacts-to-a-pact-broker-and-tagging-pacts)
* [Publishing from Go code](#publishing-from-go-code)
* [Publishing Provider Verification Results to a Pact Broker](#publishing-provider-verification-results-to-a-pact-broker)
* [Publishing from the CLI](#publishing-from-the-cli)
* [Using the Pact Broker with Basic authentication](#using-the-pact-broker-with-basic-authentication)
* [Asynchronous API Testing](#asynchronous-api-testing)
* [Consumer](#consumer)
* [Provider (Producer)](#provider-producer)
* [Pact Broker Integration](#pact-broker-integration)
* [Matching](#matching)
* [Matching on types](#matching-on-types)
* [Matching on arrays](#matching-on-arrays)
* [Matching by regular expression](#matching-by-regular-expression)
* [Match common formats](#match-common-formats)
* [Auto-generate matchers from struct tags](#auto-generate-matchers-from-struct-tags)
* [Examples](#examples)
* [HTTP APIs](#http-apis)
* [Asynchronous APIs](#asynchronous-apis)
* [Integrated examples](#integrated-examples)
* [Troubleshooting](#troubleshooting)
* [Splitting tests across multiple files](#splitting-tests-across-multiple-files)
* [Output Logging](#output-logging)
* [Contact](#contact)
* [Documentation](#documentation)
* [Troubleshooting](#troubleshooting-1)
* [Roadmap](#roadmap)
* [Contributing](#contributing)

<!-- /TOC -->

Expand Down Expand Up @@ -469,7 +469,7 @@ func TestMessageConsumer_Success(t *testing.T) {
**Explanation**:
1. The API - a contrived API handler example. Expects a User object and throws an `Error` if it can't handle it.
1. The API - a contrived API handler example. Expects a User object and throws an `Error` if it can't handle it.
* In most applications, some form of transactionality exists and communication with a MQ/broker happens.
* It's important we separate out the protocol bits from the message handling bits, so that we can test that in isolation.
1. Creates the MessageConsumer class
Expand Down Expand Up @@ -597,6 +597,50 @@ Often times, you find yourself having to re-write regular expressions for common
| `IPv6Address()` | Match string containing IP6 formatted address |
| `UUID()` | Match strings containing UUIDs |
#### Auto-generate matchers from struct tags
Furthermore, if you isolate your Data Transfer Objects (DTOs) to an adapters package so that they exactly reflect the interface between you and your provider, then you can leverage `dsl.Match` to auto-generate the expected response body in your contract tests. Under the hood, `Match` recursively traverses the DTO struct and uses `Term, Like, and EachLike` to create the contract.
This saves the trouble of declaring the contract by hand. It also maintains one source of truth. To change the consumer-provider interface, you only have to update your DTO struct and the contract will automatically follow suit.
_Example:_
```go
type DTO struct {
ID string `json:"id"`
Title string `json:"title"`
Tags []string `json:"tags" pact:"min=2"`
Date string `json:"date" pact:"example=2000-01-01,regex=^\\d{4}-\\d{2}-\\d{2}$"`
}
```

then specifying a response body is as simple as:

```go
// Set up our expected interactions.
pact.
AddInteraction().
Given("User foo exists").
UponReceiving("A request to get foo").
WithRequest(dsl.Request{
Method: "GET",
Path: "/foobar",
Headers: map[string]string{"Content-Type": "application/json"},
}).
WillRespondWith(dsl.Response{
Status: 200,
Headers: map[string]string{"Content-Type": "application/json"},
Body: Match(DTO{}), // That's it!!!
})
```

The `pact` struct tags shown above are optional. By default, dsl.Match just asserts that the JSON shape matches the struct and that the field types match.

See [dsl.Match](https://github.com/pact-foundation/pact-go/blob/master/dsl/matcher.go) for more information.

See the [matcher tests](https://github.com/pact-foundation/pact-go/blob/master/dsl/matcher_test.go)
for more matching examples.

## Examples

### HTTP APIs
Expand Down
110 changes: 110 additions & 0 deletions dsl/matcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import (
"encoding/json"
"fmt"
"log"
"reflect"
"strconv"
"strings"
"time"
)

Expand Down Expand Up @@ -306,3 +308,111 @@ func objectToString(obj interface{}) string {
return string(jsonString)
}
}

// Match recursively traverses the provided type and outputs a
// matcher string for it that is compatible with the Pact dsl.
// By default, it requires slices to have a minimum of 1 element.
// For concrete types, it uses `dsl.Like` to assert that types match.
// Optionally, you may override these defaults by supplying custom
// pact tags on your structs.
//
// Supported Tag Formats
// Minimum Slice Size: `pact:"min=2"`
// String RegEx: `pact:"example=2000-01-01,regex=^\\d{4}-\\d{2}-\\d{2}$"`
func Match(src interface{}) Matcher {
return match(reflect.TypeOf(src), getDefaults())
}

// match recursively traverses the provided type and outputs a
// matcher string for it that is compatible with the Pact dsl.
func match(srcType reflect.Type, params params) Matcher {
switch kind := srcType.Kind(); kind {
case reflect.Ptr:
return match(srcType.Elem(), params)
case reflect.Slice, reflect.Array:
return EachLike(match(srcType.Elem(), getDefaults()), params.slice.min)
case reflect.Struct:
result := make(map[string]interface{})

for i := 0; i < srcType.NumField(); i++ {
field := srcType.Field(i)
result[field.Tag.Get("json")] = match(field.Type, pluckParams(field.Type, field.Tag.Get("pact")))
}
return result
case reflect.String:
if params.str.regEx != "" {
return Term(params.str.example, params.str.regEx)
}
return Like(`"string"`)
case reflect.Bool:
return Like(true)
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
reflect.Float32, reflect.Float64:
return Like(1)
default:
panic(fmt.Sprintf("match: unhandled type: %v", srcType))
}
}

// params are plucked from 'pact' struct tags as match() traverses
// struct fields. They are passed back into match() along with their
// associated type to serve as parameters for the dsl functions.
type params struct {
slice sliceParams
str stringParams
}

type sliceParams struct {
min int
}

type stringParams struct {
example string
regEx string
}

// getDefaults returns the default params
func getDefaults() params {
return params{
slice: sliceParams{
min: 1,
},
}
}

// pluckParams converts a 'pact' tag into a pactParams struct
// Supported Tag Formats
// Minimum Slice Size: `pact:"min=2"`
// String RegEx: `pact:"example=2000-01-01,regex=^\\d{4}-\\d{2}-\\d{2}$"`
func pluckParams(srcType reflect.Type, pactTag string) params {
params := getDefaults()
if pactTag == "" {
return params
}

switch kind := srcType.Kind(); kind {
case reflect.Slice:
if _, err := fmt.Sscanf(pactTag, "min=%d", &params.slice.min); err != nil {
triggerInvalidPactTagPanic(pactTag, err)
}
case reflect.String:
components := strings.Split(pactTag, ",regex=")

if len(components) != 2 {
triggerInvalidPactTagPanic(pactTag, fmt.Errorf("invalid format: unable to split on ',regex='"))
} else if len(components[1]) == 0 {
triggerInvalidPactTagPanic(pactTag, fmt.Errorf("invalid format: regex must not be empty"))
} else if _, err := fmt.Sscanf(components[0], "example=%s", &params.str.example); err != nil {
triggerInvalidPactTagPanic(pactTag, err)
}

params.str.regEx = strings.Replace(components[1], `\`, `\\`, -1)
}

return params
}

func triggerInvalidPactTagPanic(tag string, err error) {
panic(fmt.Sprintf("match: encountered invalid pact tag %q . . . parsing failed with error: %v", tag, err))
}
Loading

0 comments on commit 16a9146

Please sign in to comment.