Skip to content

Commit

Permalink
feat(matching): add a number of common matchers to DSL
Browse files Browse the repository at this point in the history
Added the following matchers:

* `boolean` - Match a boolean value (using equality)
* `integer` - Will match all numbers that are integers (both ints and longs)
* `decimal` - Will match all real numbers (floating point and decimal)
* `hexadecimal` - Will match all hexadecimal encoded strings
* `iso8601Date` - Will match string containing basic ISO8601 dates (e.g. 2016-01-01)
* `iso8601DateTime` - Will match string containing ISO 8601 formatted dates (e.g. 2015-08-06T16:53:10+01:00)
* `iso8601DateTimeWithMillis` - Will match string containing ISO 8601 formatted dates, enforcing millisecond precision (e.g. 2015-08-06T16:53:10.123+01:00)
* `rfc3339Timestamp` - Will match a string containing an RFC3339 formatted timestamp (e.g. Mon, 31 Oct 2016 15:21:41 -0400)
* `iso8601Time` - Will match string containing times (e.g. T22:44:30.652Z)
* `ipv4Address` - Will match string containing IP4 formatted address
* `ipv6Address` - Will match string containing IP6 formatted address
* `uuid` - Will match strings containing UUIDs

See #120.
  • Loading branch information
mefellows committed Dec 5, 2017
1 parent 7666498 commit 4259171
Show file tree
Hide file tree
Showing 3 changed files with 328 additions and 72 deletions.
95 changes: 57 additions & 38 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,11 @@ how to get going.
- [API with Authorization](#api-with-authorization)
- [Publishing Verification Results to a Pact Broker](#publishing-verification-results-to-a-pact-broker)
- [Publishing Pacts to a Broker](#publishing-pacts-to-a-broker)
- [Flexible Matching](#flexible-matching)
- [Match by regular expression](#match-by-regular-expression)
- [Matching](#matching)
- [Match common formats](#match-common-formats)
- [Match based on type](#match-based-on-type)
- [Match based on arrays](#match-based-on-arrays)
- [Match by regular expression](#match-by-regular-expression)
- [Tutorial (60 minutes)](#tutorial-60-minutes)
- [Examples](#examples)
- [Using Pact in non-Node environments](#using-pact-in-non-node-environments)
Expand Down Expand Up @@ -314,49 +315,34 @@ pact.publishPacts(opts)).then(function () {
});
```

### Flexible Matching
### Matching

Matching makes your tests more expressive making your tests less brittle.

Flexible matching makes your tests more expressive making your tests less brittle.
Rather than use hard-coded values which must then be present on the Provider side,
you can use regular expressions and type matches on objects and arrays to validate the
structure of your APIs.

Read more about using regular expressions and type based matching [here][https://github.com/realestate-com-au/pact/wiki/Regular-expressions-and-type-matching-with-Pact] before continuing.

_NOTE: Make sure to start the mock service via the `Pact` declaration with the option `specification: 2` to get access to these features._

For simplicity, we alias the main matches to make our code more readable:

#### Match by regular expression

The underlying mock service is written in Ruby, so the regular expression must be in a Ruby format, not a Javascript format.

```javascript
const { term } = pact

provider.addInteraction({
state: 'Has some animals',
uponReceiving: 'a request for an animal',
withRequest: {
method: 'GET',
path: '/animals/1'
},
willRespondWith: {
status: 200,
headers: {
'Content-Type': 'application/json; charset=utf-8'
},
body: {
id: 100,
name: "billy",
'gender': term({
matcher: 'F|M',
generate: 'F'
}),
}
}
})
```
#### Match common formats

Often times, you find yourself having to re-write regular expressions for common formats. We've created a number of them for you to save you the time:

| method | description |
|--------|-------------|
| `boolean` | Match a boolean value (using equality) |
| `integer` | Will match all numbers that are integers (both ints and longs)|
| `decimal` | Will match all real numbers (floating point and decimal)|
| `hexadecimal` | Will match all hexadecimal encoded strings |
| `iso8601Date` | Will match string containing basic ISO8601 dates (e.g. 2016-01-01)|
| `iso8601DateTime` | Will match string containing ISO 8601 formatted dates (e.g. 2015-08-06T16:53:10+01:00)|
| `iso8601DateTimeWithMillis` | Will match string containing ISO 8601 formatted dates, enforcing millisecond precision (e.g. 2015-08-06T16:53:10.123+01:00)|
| `rfc3339Timestamp` | Will match a string containing an RFC3339 formatted timestapm (e.g. Mon, 31 Oct 2016 15:21:41 -0400)|
| `iso8601Time` | Will match string containing times (e.g. T22:44:30.652Z)|
| `ipv4Address` | Will match string containing IP4 formatted address |
| `ipv6Address` | Will match string containing IP6 formatted address |
| `uuid` | Will match strings containing UUIDs |

#### Match based on type

Expand Down Expand Up @@ -452,6 +438,39 @@ provider.addInteraction({
})
```

#### Match by regular expression

If none of the above matchers or formats work, you can write your own regex matcher.

The underlying mock service is written in Ruby, so the regular expression must be in a Ruby format, not a Javascript format.

```javascript
const { term } = pact

provider.addInteraction({
state: 'Has some animals',
uponReceiving: 'a request for an animal',
withRequest: {
method: 'GET',
path: '/animals/1'
},
willRespondWith: {
status: 200,
headers: {
'Content-Type': 'application/json; charset=utf-8'
},
body: {
id: 100,
name: "billy",
'gender': term({
matcher: 'F|M',
generate: 'F'
}),
}
}
})
```

## Tutorial (60 minutes)

Learn everything in Pact JS in 60 minutes: https://github.com/DiUS/pact-workshop-js
Expand Down
225 changes: 218 additions & 7 deletions src/dsl/matchers.spec.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,39 @@
import * as chai from 'chai';
const expect = require('chai').expect;
import { Interaction } from './interaction';
import { term, somethingLike, eachLike } from './matchers';
import {
term, somethingLike, eachLike, validateExample,
ISO8601_DATE_FORMAT, uuid, ipv4Address, ipv6Address, hexadecimal,
boolean, integer, decimal, rfc3339Timestamp, iso8601Date, iso8601DateTime, iso8601Time,
iso8601DateTimeWithMillis
} from './matchers';

describe('Matcher', () => {

describe('#validateExample', () => {
describe('when given a valid regex', () => {
describe('and a matching example', () => {
it('should return true', () => {
expect(validateExample('2010-01-01', ISO8601_DATE_FORMAT)).to.eql(true);
})
});
describe('and a failing example', () => {
it('should return false', () => {
expect(validateExample('not a date', ISO8601_DATE_FORMAT)).to.eql(false);
})
});
});
describe('when given an invalid regex', () => {
it('should return an error', () => {
expect(() => {
validateExample('', 'abc(')
}).to.throw(Error);
})
});
});

describe('#term', () => {
describe('when provided a term', () => {
describe('when given a valid regular expression and example', () => {
it('should return a serialized Ruby object', () => {
const expected = {
'json_class': 'Pact::Term',
Expand All @@ -29,16 +56,16 @@ describe('Matcher', () => {
});
});

describe('when not provided with a valid term', () => {
describe('when not provided with a valid expression', () => {
const createTheTerm = function (badArg: any) {
return () => {
term(badArg);
};
};

describe('when no term is provided', () => {
it.skip('should throw an Error', () => {
// expect(createTheTerm()).to.throw(Error)
it('should throw an Error', () => {
expect(createTheTerm.call({})).to.throw(Error)
});
});

Expand All @@ -51,6 +78,17 @@ describe('Matcher', () => {
});
});
});

describe('when given an example that doesn\'t match the regular expression', () => {
it('should fail with an error', () => {
expect(() => {
term({
generate: 'abc',
matcher: ISO8601_DATE_FORMAT
})
}).to.throw(Error);
});
});
});

describe('#somethingLike', () => {
Expand All @@ -74,8 +112,8 @@ describe('Matcher', () => {
};

describe('when no value is provided', () => {
it.skip('`should throw an Error', () => {
// expect(createTheValue()).to.throw(Error)
it('`should throw an Error', () => {
expect(createTheValue.call({})).to.throw(Error)
});
});

Expand Down Expand Up @@ -300,4 +338,177 @@ describe('Matcher', () => {
});
});
});

describe('#uuid', () => {
describe('when given a valid UUID', () => {
it('should not fail', () => {
expect(uuid('ce118b6e-d8e1-11e7-9296-cec278b6b50a')).to.be.an('object')
expect(uuid()).to.be.an('object')
})
});
describe('when given an invalid UUID', () => {
it('should return an error', () => {
expect(() => {
uuid('abc');
}).to.throw(Error);
});
});
});

describe('#ipv4Address', () => {
describe('when given a valid ipv4Address', () => {
it('should not fail', () => {
expect(ipv4Address('127.0.0.1')).to.be.an('object')
expect(ipv4Address()).to.be.an('object')
})
});
describe('when given an invalid ipv4Address', () => {
it('should return an error', () => {
expect(() => {
ipv4Address('abc');
}).to.throw(Error);
});
});
});

describe('#ipv6Address', () => {
describe('when given a valid ipv6Address', () => {
it('should not fail', () => {
expect(ipv6Address('::1')).to.be.an('object')
expect(ipv6Address('2001:0db8:85a3:0000:0000:8a2e:0370:7334')).to.be.an('object')
expect(ipv6Address()).to.be.an('object')
})
});
describe('when given an invalid ipv6Address', () => {
it('should return an error', () => {
expect(() => {
ipv6Address('abc');
}).to.throw(Error);
});
});
});

describe('#hexadecimal', () => {
describe('when given a valid hexadecimal', () => {
it('should not fail', () => {
expect(hexadecimal('6F')).to.be.an('object')
expect(hexadecimal()).to.be.an('object')
})
});
describe('when given an invalid hexadecimal', () => {
it('should return an error', () => {
expect(() => {
hexadecimal('x1');
}).to.throw(Error);
});
});
});

describe('#boolean', () => {
describe('when used it should create a JSON object', () => {
it('should not fail', () => {
expect(boolean()).to.be.an('object')
})
});
});

describe('#decimal', () => {
describe('when given a valid decimal', () => {
it('should not fail', () => {
expect(decimal(10.1)).to.be.an('object')
expect(decimal()).to.be.an('object')
})
});
});

describe('#integer', () => {
describe('when given a valid integer', () => {
it('should not fail', () => {
expect(integer(10)).to.be.an('object')
expect(integer()).to.be.an('object')
})
});
});

describe('Date Matchers', () => {
describe('#rfc3339Timestamp', () => {
describe('when given a valid rfc3339Timestamp', () => {
it('should not fail', () => {
expect(rfc3339Timestamp('Mon, 31 Oct 2016 15:21:41 -0400')).to.be.an('object')
expect(rfc3339Timestamp()).to.be.an('object')
})
});
describe('when given an invalid rfc3339Timestamp', () => {
it('should return an error', () => {
expect(() => {
rfc3339Timestamp('abc');
}).to.throw(Error);
});
});
});

describe('#iso8601Time', () => {
describe('when given a valid iso8601Time', () => {
it('should not fail', () => {
expect(iso8601Time('T22:44:30.652Z')).to.be.an('object')
expect(iso8601Time()).to.be.an('object')
})
});
describe('when given an invalid iso8601Time', () => {
it('should return an error', () => {
expect(() => {
iso8601Time('abc');
}).to.throw(Error);
});
});
});

describe('#iso8601Date', () => {
describe('when given a valid iso8601Date', () => {
it('should not fail', () => {
expect(iso8601Date('2017-12-05')).to.be.an('object')
expect(iso8601Date()).to.be.an('object')
})
});
describe('when given an invalid iso8601Date', () => {
it('should return an error', () => {
expect(() => {
iso8601Date('abc');
}).to.throw(Error);
});
});
});

describe('#iso8601DateTime', () => {
describe('when given a valid iso8601DateTime', () => {
it('should not fail', () => {
expect(iso8601DateTime('2015-08-06T16:53:10+01:00')).to.be.an('object')
expect(iso8601DateTime()).to.be.an('object')
})
});
describe('when given an invalid iso8601DateTime', () => {
it('should return an error', () => {
expect(() => {
iso8601DateTime('abc');
}).to.throw(Error);
});
});
});

describe('#iso8601DateTimeWithMillis', () => {
describe('when given a valid iso8601DateTimeWithMillis', () => {
it('should not fail', () => {
expect(iso8601DateTimeWithMillis('2015-08-06T16:53:10.123+01:00')).to.be.an('object')
expect(iso8601DateTimeWithMillis()).to.be.an('object')
})
});
describe('when given an invalid iso8601DateTimeWithMillis', () => {
it('should return an error', () => {
expect(() => {
iso8601DateTimeWithMillis('abc');
}).to.throw(Error);
});
});
});
});
});
Loading

0 comments on commit 4259171

Please sign in to comment.