pact-gen-ts
Version:
Generating pact files from typescript definitions
396 lines (315 loc) • 11.8 kB
Markdown
# Pact-gen-ts
Pact-gen-ts is a tool for generating contracts using TypeScript type definitions and custom JSDoc tags.
It's an alternative to the [pact-js](https://github.com/pact-foundation/pact-js) package but without the necessity for writing separate tests.
It provides automated, low maintenance and more flexible way to generate contracts according to [Pact specification version 2](https://github.com/pact-foundation/pact-specification).
## Installation and usage
You can install pact-gen-ts using npm:
```bash
npm install pact-gen-ts --save-dev
```
or yarn:
```bash
yarn add --dev pact-gen-ts
```
Next you should create a minimal `pacts.config.js` or `pacts.config.cjs` configuration file in the root directory:
```js
module.exports = {
consumer: 'consumer-name',
providers: [
{
provider: 'some-provider',
files: ['src/api/**/*.ts'],
},
],
};
```
where `files` property will be an array of glob patterns pointing to API functions definitions.
After that pact-gen-ts is ready, now you need to mark all API functions which will be analysed:
```ts
/**
* @pact
*/
function fetchComments() {
// ...
}
```
The last thing is to execute the command:
```bash
pact-gen-ts
```
which does the analysis and generates pacts in JSON format inside (by default) `./pacts` directory.
## Compatibility with TypeScript
Due to TypeScript's occasional changes to its compiler API and not following semantic versioning in their releases, the latest versions of pact-gen-ts can only guarantee compatibility with the latest versions of TypeScript.
If you're limited to historical versions of TypeScript, you should install a corresponding version of pact-gen-ts. The below table presents what TS versions pact-gen-ts will work with:
| pact-gen-ts | TypeScript |
| --------------- | ---------- |
| 0.8 | 4.1 - 4.2 |
| 0.9 - 0.9.3 | 4.5 - 4.6 |
| 0.9.4 - 0.10.0 | 4.7 - 4.8 |
| 0.11.0 | 4.9 |
| 0.12.0 | 5.0 |
| 0.13.0 | 5.1 |
| 0.14.0 | 5.2 - 5.3 |
| 0.15.0 | 5.4 |
| 0.16.0 | 5.5 - 5.6 |
| 0.17.0 - 0.19.0 | \>=5.7 |
## Configuration
Pact-gen-ts uses configuration stored in `pacts.config.js` file in project's root directory:
```js
module.exports = {
consumer: 'consumer-name',
buildDir: 'pacts',
verbose: true,
providers: [
{
provider: 'provider-name',
files: ['src/api/firstProvider/*.ts'],
queryArrayFormat: 'indices',
requestHeaders: {
authorization: 'auth',
},
responseHeaders: {
'Content-Type': 'application/json',
},
},
],
};
```
### Options
| Option | Required | Default | Description |
| ------------------------------ | :------: | :----------: | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `consumer` | Yes | - | Consumer's name |
| `providers[].provider` | Yes | - | Provider's name |
| `providers[].files` | Yes | - | Array of glob patterns where API functions are defined |
| `providers[].requestHeaders` | No | - | Request headers shared across all requests |
| `providers[].responseHeaders` | No | - | Response headers shared across all responses |
| `providers[].queryArrayFormat` | No | `"brackets"` | Sets separator for array in query - possible options are `"indices"`, `"brackets"`, `"comma"` and `"repeat"` [(source)](https://github.com/ljharb/qs#stringifying). The default value is `brackets`. |
| `buildDir` | No | `./pacts` | Directory where generated pacts will be placed |
| `verbose` | No | `false` | If set to `true` additional information during pacts generating process will be logged |
You can specify common config shared between providers in **pacts.config.js**:
```js
module.exports = {
commonConfigForProviders: {
queryArrayFormat: 'indices',
requestHeaders: {
authorization: 'auth',
},
responseHeaders: {
'Content-Type': 'application/json',
},
},
providers: [
{
provider: 'first-provider',
files: ['src/api1/**/*.ts'],
},
{
provider: 'second-provider',
files: ['src/api2/**/*.ts'],
},
{
provider: 'third-provider',
files: ['src/api3/**/*.ts'],
// you can override common config in provider config
queryArrayFormat: 'comma',
},
],
};
```
## Integrations
#### Axios - `@pact-axios`
Sets REST method, expected body for the current response, expected body for current request and query based on axios definitions.
```ts
/**
* @pact
* @pact-axios
* @pact-path /api
*/
async function fetchComments(commentId: string) {
const {data} = await axios.post<string>('/api', {commentId});
// ...
}
```
**IMPORTANT** - If axios function does not return any type explicitly it is needed to set `<void>` as an axios return type
```ts
/**
* @pact
* @pact-axios
* @pact-path /api
*/
async function fetchComments(commentId: string) {
await axios.post<void>('/api', {commentId});
}
```
## Pact interaction options
These JSDoc custom tags are used to adjust generated pact interactions.
#### `@pact-method`
Sets REST method (GET, POST, PUT, PATCH, DELETE etc.).
```ts
/**
* @pact
* @pact-method GET
*/
function fetchComments() {
// ...
}
```
#### `@pact-path`
Sets path.
```ts
/**
* @pact
* @pact-path /api/images/100
*/
function fetchImage(imageId: number) {
// ...
}
```
#### `@pact-description`
Sets description, if not provided, description is set using name of the function / variable / property.
```ts
/**
* @pact
* @pact-description "request to get comments"
*/
function fetchComments() {
// ...
}
```
#### `@pact-response-status`
Sets response status, if not provided, it is set based on given HTTP method.
```ts
/**
* @pact
* @pact-response-status 200
*/
function fetchComments() {
// ...
}
```
#### `@pact-request-header`
Adds a header to the current request, can override option defined in `pacts.config.js`.
```ts
/**
* @pact
* @pact-request-header "Content-Type" "application/pdf"
*/
function fetchImage(imageId: number) {
// ...
}
```
#### `@pact-response-header`
Adds a header to the current response, can override option defined in `pacts.config.js`.
```ts
/**
* @pact
* @pact-response-header "Content-Type" "application/pdf"
*/
function fetchImage(imageId: number) {
// ...
}
```
#### `@pact-response-body`
Sets expected body for the current response.
```ts
/**
* @pact
*/
async function fetchComments() {
// ...
const response = await axios.get<string>('/api');
/** @pact-response-body */
const data = response.data;
// ...
}
```
**IMPORTANT** - JSDoc has to be applied to separate variable - **not** directly to axios response
```ts
async function fetchComments() {
// ...
/** @pact-response-body */ -WRONG!;
const response = await axios.get<string>('/api');
/** @pact-response-body */ -CORRECT;
const data = response.data;
// ...
}
```
#### `@pact-request-body`
Sets expected body for current request.
```ts
function addComment(/** @pact-request-body */ newComment: NewComment) {
// ...
}
interface NewComment {
content: string;
postId: string;
}
```
or
```js
function addComment(postId: string, commentContent: string) {
/** @pact-request-body */
const newComment = {
postId,
commentContent,
};
// ...
}
```
#### `@pact-query`
Sets query, **IMPORTANT** - JSDoc tag has to be applied to an object - not a primitive value.
Array separator format can be set using `queryArrayFormat` in providers options.
```ts
function fetchComments(/** @pact-query */ query: Query) {
// ...
}
interface Query {
fromUser: string;
postId: string;
}
```
or
```ts
function fetchComments(pageNo: string) {
/** @pact-query */
const params = {
pageNo,
};
// ...
}
```
### Pact matchers
Typescript types can describe the shape of the data and define possible values a variable can store. Pacts definition require specific values, that's why for some individual cases additional information needs to be added.
For example a type `string` without any modifications will be replaced with simple `text` which can be later matched by type. Sometimes that's not enough - the matcher needs to be more specific, for instance instead of simple `text` we need a string in a particular format like `name@example.com` - that's where a `@pact-matcher` tag is used.
Pact-matchers are used in the type/interface definition:
```ts
interface CommentDTO {
id: number;
/** @pact-matcher email */
user: string;
}
```
Provided common matchers:
| Pact matcher | Result |
| ----------------------------------------------- | ----------------------------------- |
| `/** @pact-matcher email */` | email@example.com |
| `/** @pact-matcher iso-date */` | 2021-04-13 |
| `/** @pact-matcher iso-datetime */` | 2021-04-13T10:14:53+01:00 |
| `/** @pact-matcher iso-datetime-with-millis */` | 2021-04-13T10:14:53.123+01:00 |
| `/** @pact-matcher iso-time */` | T10.14.53.342Z |
| `/** @pact-matcher timestamp */` | Tue, 13 Apr 2021 10:14:53 -0400 |
| `/** @pact-matcher uuid */` | ce11b6e-d8e1-11e7-9296-cec278b6b50a |
| `/** @pact-matcher ipv4 */` | 127.0.0.13 |
| `/** @pact-matcher ipv6 */` | ::ffff:192.0.2.128 |
| `/** @pact-matcher hex */` | A4C3Ff |
If that's not enough you can easily provide own value using `/** @pact-example */`:
```ts
interface Address {
city: string;
address: string;
/** @pact-example 99-400 */
postCode: string;
/** @pact-example 45 */
age: number;
}
```