Skip to content

Commit b916e01

Browse files
chore: update docs to better describe library uses and usage
1 parent bdc0756 commit b916e01

File tree

3 files changed

+209
-54
lines changed

3 files changed

+209
-54
lines changed

README.md

Lines changed: 109 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -3,83 +3,129 @@
33
[![ci](https://github.com/matthewshirley/pact_dart/actions/workflows/ci.yml/badge.svg)](https://github.com/matthewshirley/pact_dart/actions/workflows/ci.yml)
44
[![codecov](https://codecov.io/gh/matthewshirley/pact_dart/branch/main/graph/badge.svg?token=N7495X6QCL)](https://codecov.io/gh/matthewshirley/pact_dart)
55

6-
⚠️ WIP Package - API is not yet confirmed ⚠️
6+
[Pact][pact-docs] is a contract testing tool to help you replace expensive and brittle end-to-end integration tests with fast, reliable and easy to debug unit tests. This framework provides a Dart DSL for generating Pact contracts. It implements [Pact Specification v3][pact-specification-v3] using the `Pact FFI Library`.
77

8-
This library provides a Dart DSL for generating Pact contracts. It implements [Pact Specification v3](https://github.com/pact-foundation/pact-specification/tree/version-3) by taking advantage of the pact_ffi library.
8+
## Documentation
99

10-
### Installation
10+
This readme offers an basic introduction to the library. View more documentation on Pact at https://docs.pact.io/.
1111

12-
```bash
13-
dart pub add pact_dart
12+
- [Installation](#installation)
13+
- [Basic Usage](#usage)
14+
- [Consumer Documentation](./docs/consumer.md)
15+
16+
## Need Help
17+
18+
- [Join](<(http://slack.pact.io)>) our community [slack workspace](http://pact-foundation.slack.com/).
19+
- Stack Overflow: https://stackoverflow.com/questions/tagged/pact
20+
- Say 👋 on Twitter: [@pact_up]
21+
22+
## Installation
23+
24+
```shell
25+
# install pact_dart as a dev dependency
26+
dart pub add --dev pact_dart
27+
28+
# download and install the required libraries
1429
dart run pact_dart:install
30+
31+
# 🚀 now write some tests!
1532
```
1633

17-
### Example
34+
<details><summary>Flutter Instructions</summary>
1835

19-
```dart
20-
import 'package:pact_dart/pact_dart.dart';
36+
### Flutter Installation
2137

22-
final pact = PactMockService('test-ffi-consumer','test-ffi-provider');
38+
```bash
39+
# install pact_dart as a dev dependency
40+
flutter pub add --dev pact_dart
2341

24-
pact
25-
.newInteraction()
26-
.given('a alligator exists', params: { 'name': 'Betsy' })
27-
.andGiven('the alligators were recently fed')
28-
.uponReceiving('a request for an alligator')
29-
.withRequest('GET', '/alligator')
30-
.willRespondWith(200, body: { 'name': 'Betsy' }});
42+
# download and install the required libraries
43+
flutter pub run pact_dart:install
3144

32-
pact.run(secure: false);
45+
# 🚀 now write some tests!
46+
```
47+
48+
</details>
3349

34-
final uri = Uri.parse('http://localhost:1235/alligator');
35-
final res = await http.get(uri);
50+
<details><summary>Manual Installation Instructions</summary>
3651

37-
expect(jsonDecode(res.body)['name'], equals('Betsy'));
52+
### Modify Library Location
3853

39-
pact.writePactFile(overwrite: true);
54+
By default, the `Pact FFI Library` is installed to `/usr/local/lib` on macOS and Linux. However, you can use the `PACT_DART_LIB_DOWNLOAD_PATH` environment variable to modify the installation path.
55+
56+
```
57+
PACT_DART_LIB_DOWNLOAD_PATH=/app/my-other-location dart run pact_dart:install
4058
```
4159

42-
### Matching
60+
### Manual Installation
61+
62+
Download the latest `Pact FFI Library` [libraries] for your OS, and install onto a standard library search path (for example, we suggest: `/usr/local/lib` on OSX/Linux):
63+
64+
Ensure you have the correct extension for your OS:
4365

44-
`pact_dart` supports request/response [matching techniques](https://docs.pact.io/getting_started/matching/) as defined in the [Pact Specification v3](https://github.com/pact-foundation/pact-specification/tree/version-3).
66+
- For Mac OSX: `.dylib`
67+
- For Linux: `.so`
68+
- For Windows: `.dll`
69+
70+
```sh
71+
wget https://github.com/pact-foundation/pact-reference/releases/download/libpact_ffi-v0.0.2/libpact_ffi-osx-x86_64.dylib.gz
72+
gunzip libpact_ffi-osx-x86_64.dylib.gz
73+
mv libpact_ffi-osx-x86_64.dylib /usr/local/lib/libpact_ffi.dylib
74+
```
75+
76+
</details>
77+
78+
## Usage
79+
80+
### Writing a Consumer test
81+
82+
Pact is a consumer-driven contract testing tool, which is a fancy way of saying that the API `Consumer` writes a test to set out its assumptions and needs of its API `Provider`(s). By unit testing our API client with Pact, it will produce a `contract` that we can share to our `Provider` to confirm these assumptions and prevent breaking changes.
83+
84+
In this example, we are testing the users repository that communicates with the `/users` resource of a HTTP service. The repository has a single method `fetchAll()` that will return a list of users.
4585

4686
```dart
87+
import 'package:pact_dart/pact_dart.dart';
88+
89+
final pact = PactMockService('FlutterConsumer','APIService');
90+
4791
pact
4892
.newInteraction()
49-
.uponReceiving('a request to create an alligator named betsy')
50-
.withRequest('POST', '/alligator', body: {
51-
'alligator': {
52-
'name': PactMatchers.EqualTo('Betsy'),
53-
'isHungry': PactMatchers.SomethingLike(true),
54-
'countOfTeeth': PactMatchers.IntegerLike(80),
55-
'countOfHumansScared': PactMatchers.DecimalLike(12.5),
56-
'favouriteFood': PactMatchers.Includes('Pineapple'),
57-
'status': PactMatchers.Null(),
58-
'email': PactMatchers.Term(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}$', '[email protected]'),
59-
'friends': PactMatchers.EachLike(['Beth'], min: 1, max: 5)
60-
}
61-
}).willRespondWith(201);
62-
63-
final uri = Uri.parse('http://localhost:1235/alligator');
64-
final res = await http.post(uri,
65-
headers: {'Content-Type': 'application/json'},
66-
body: jsonEncode({
67-
'alligator': {
68-
'name': 'Betsy',
69-
'isHungry': false,
70-
'countOfTeeth': 78,
71-
'countOfHumansScared': 100.5,
72-
'favouriteFood': ['Pineapple', 'Kibble', 'Human'],
73-
'status': null,
74-
'email': '[email protected]',
75-
'friends': ['Beth', 'Susan', 'Graham', 'Michael', 'Chloe']
76-
}
93+
.given('a user exists', params: {'first_name': 'Betsy', 'last_name': 'Tester'})
94+
.andGiven('')
95+
.uponReceiving('a request for all users')
96+
.withRequest('GET', '/users')
97+
.willRespondWith(200, body: {
98+
// Matchers are used here as we care about the types and structure of the response and not the exact values.
99+
'page': PactMatchers.SomethingLike(1),
100+
'per_page': PactMatchers.SomethingLike(20),
101+
'total': PactMatchers.IntegerLike(20),
102+
'total_pages': PactMatchers.SomethingLike(3),
103+
'data': PactMatchers.EachLike([
104+
{
105+
'id': PactMatchers.uuid('f3a9cf4a-92d7-4aae-a945-63a6440b528b'),
106+
'first_name': PactMatchers.SomethingLike('Betsy'),
107+
'last_name': PactMatchers.SomethingLike('Tester'),
108+
'salary': PactMatchers.DecimalLike(125000.00)
77109
}
78-
)
79-
);
110+
])
111+
});
112+
113+
pact.run(secure: false);
114+
115+
final loginRepository = UsersRepository();
116+
final users = await loginRepository.fetchAll();
117+
118+
expect(users.count, equals(20));
119+
expect(users[0].first_name, equals('Betsy'));
120+
expect(users[0].last_name, equals('Tester'));
121+
122+
pact.writePactFile();
123+
pact.reset();
80124
```
81125

82-
### Feature support
126+
## Compatibility
127+
128+
<details><summary>Feature Compatibility</summary>
83129

84130
| Feature | Supported |
85131
| ---------------------------------------------------------------------- | --------- |
@@ -111,3 +157,12 @@ pact
111157
- ✅ -- Implemented
112158
- 🔨 -- Partially implemented
113159
- ❌ -- Not implemented
160+
161+
</details>
162+
163+
[pact-docs]: https://docs.pact.io
164+
[pact-specification-v3]: https://github.com/pact-foundation/pact-specification/tree/version-3
165+
[slack]: https://slack.pact.io
166+
[pact website]: https://docs.pact.io/
167+
[slack channel]: https://pact-foundation.slack.com
168+
[@pact_up]: https://twitter.com/pact_up

docs/consumer.md

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
# Consumer Tests
2+
3+
## Contract Testing Process (HTTP)
4+
5+
Pact is a consumer-driven contract testing tool, which is a fancy way of saying that the API `Consumer` writes a test to set out its assumptions and needs of its API `Provider`(s). By unit testing our API client with Pact, it will produce a `contract` that we can share to our `Provider` to confirm these assumptions and prevent breaking changes.
6+
7+
The process looks like this:
8+
9+
![diagram](./diagrams/summary.png)
10+
11+
1. The consumer writes a unit test of its behaviour using a Mock provided by Pact
12+
2. Pact writes the interactions into a contract file (as a JSON document)
13+
3. The consumer publishes the contract to a broker (or shares the file in some other way)
14+
4. Pact retrieves the contracts and replays the requests against a locally running provider
15+
5. The provider should stub out its dependencies during a Pact test, to ensure tests are fast and more deterministic.
16+
17+
In this document, we will cover steps 1-3.
18+
19+
## Writing a Consumer test
20+
21+
In this example, we are going to be testing our Product API client, responsible for communicating with the ProductAPI over HTTP. It currently has a single method getProduct(id) that will return a product.
22+
23+
24+
### Example
25+
26+
27+
```dart
28+
import 'package:pact_dart/pact_dart.dart';
29+
30+
final pact = PactMockService('ProductAPIConsumer','ProductAPI');
31+
32+
pact
33+
.newInteraction()
34+
.given('A Product with ID 10 exists')
35+
.uponReceiving('A request for Product 10')
36+
.withRequest('GET', '/product/10')
37+
.willRespondWith(200, body: {
38+
'id': PactMatchers.EqualTo('10'),
39+
'name': PactMatchers.SomethingLike('All Dressed Chips'),
40+
'description': PactMatchers.SomethingLike('A masterpiece.'),
41+
'price': PactMatchers.DecimalLike(19.99)
42+
});
43+
44+
pact.run(secure: false);
45+
46+
final productRepository = ProductRepository();
47+
final product = await productRepository.getProduct('10');
48+
49+
expect(product.id, equals('10'));
50+
51+
pact.writePactFile();
52+
pact.reset();
53+
54+
```
55+
56+
### Matching
57+
58+
In addition to matching on exact values, there are a number of useful matching functions
59+
in the `matching` package that can increase the expressiveness of your tests and reduce brittle
60+
test cases.
61+
62+
Rather than use hard-coded values which must then be present on the Provider side,
63+
you can use regular expressions and type matches on objects and arrays to validate the
64+
structure of your APIs.
65+
66+
Matchers can be used on the `Body`, `Headers`, `Path` and `Query` fields of the request,
67+
and the `Body` and `Headers` on the response.
68+
69+
_NOTE: Some matchers are only compatible with the V3 interface, and must not be used with a V2 Pact. Your test will panic if this is attempted_
70+
71+
| Matcher | Min. Compatibility | Description |
72+
| ------------------------------ | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
73+
| `SomethingLike(content)` | V2 | Tells Pact that the value itself is not important, as long as the element _type_ (valid JSON number, string, object etc.) itself matches. |
74+
| `Term(matcher, regex)` | V2 | Tells Pact that the value should match using a given regular expression, using `example` in mock responses. `example` must be a string. |
75+
| `EachLike(content, min, max?)` | V2 | Tells Pact that the value should be an array type, consisting of elements like those passed in. `min` must be >= 1. `content` may be any valid JSON value: e.g. strings, numbers and objects. |
76+
| `EqualTo(content)` | V3 | Matchers cascade, equality resets the matching process back to exact values |
77+
| `Includes(content)` | V3 | Checks if the given string is contained by the actual value |
78+
| `IntegerLike(int)` | V3 | Match all numbers that are integers (both ints and longs) |
79+
| `DecimalLike(decimal)` | V3 | Match all real numbers (floating point and decimal) |
80+
81+
#### Match common formats
82+
83+
| method | Min. Compatibility | description |
84+
| --------- | ------------------ | --------------------------------- |
85+
| `uuid()` | V2 | Match strings containing UUIDs |
86+
| `email()` | V2 | Match strings containing an email |
87+
88+
### Managing Test Data (using Provider States)
89+
90+
Each interaction in a pact should be verified in isolation, with no context maintained from the previous interactions. Tests that depend on the outcome of previous tests are brittle and hard to manage.
91+
Provider states is the feature that allows you to test a request that requires data to exist on the provider.
92+
93+
Read more about [provider states](https://docs.pact.io/getting_started/provider_states/)
94+
95+
There are several ways to define a provider state:
96+
97+
1. Using the `given(state)` method passing in a plain string.
98+
2. Using the `given(state, param: params)` method, passing in a string description and a hash of parameters to be used by the provider during verification.
99+
100+
For V3 tests, these methods may be called multiple times, resulting in more than 1 state for a given interaction. You may also use the `andGiven` method for a more readable syntax.

docs/diagrams/summary.png

395 KB
Loading

0 commit comments

Comments
 (0)