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 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.
You can install pact-gen-ts using npm:
npm install pact-gen-ts --save-dev
or yarn:
yarn add --dev pact-gen-ts
Next you should create a minimal pacts.config.js
configuration file in the root directory:
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:
/**
* @pact
*/
function fetchComments() {
// ...
}
The last thing is to execute the command:
pact-gen-ts
which does the analysis and generates pacts in JSON format inside (by default) ./pacts
directory.
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 |
Pact-gen-ts uses configuration stored in pacts.config.js
file in project's root directory:
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',
},
},
],
};
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). 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:
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',
},
],
};
Sets REST method, expected body for the current response, expected body for current request and query based on axios definitions.
/**
* @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
/**
* @pact
* @pact-axios
* @pact-path /api
*/
async function fetchComments(commentId: string) {
await axios.post<void>('/api', {commentId});
}
These JSDoc custom tags are used to adjust generated pact interactions.
Sets REST method (GET, POST, PUT, PATCH, DELETE etc.).
/**
* @pact
* @pact-method GET
*/
function fetchComments() {
// ...
}
Sets path.
/**
* @pact
* @pact-path /api/images/100
*/
function fetchImage(imageId: number) {
// ...
}
Sets description, if not provided, description is set using name of the function / variable / property.
/**
* @pact
* @pact-description "request to get comments"
*/
function fetchComments() {
// ...
}
Sets response status, if not provided, it is set based on given HTTP method.
/**
* @pact
* @pact-response-status 200
*/
function fetchComments() {
// ...
}
Adds a header to the current request, can override option defined in pacts.config.js
.
/**
* @pact
* @pact-request-header "Content-Type" "application/pdf"
*/
function fetchImage(imageId: number) {
// ...
}
Adds a header to the current response, can override option defined in pacts.config.js
.
/**
* @pact
* @pact-response-header "Content-Type" "application/pdf"
*/
function fetchImage(imageId: number) {
// ...
}
Sets expected body for the current response.
/**
* @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
async function fetchComments() {
// ...
/** @pact-response-body */ -WRONG!;
const response = await axios.get<string>('/api');
/** @pact-response-body */ -CORRECT;
const data = response.data;
// ...
}
Sets expected body for current request.
function addComment(/** @pact-request-body */ newComment: NewComment) {
// ...
}
interface NewComment {
content: string;
postId: string;
}
or
function addComment(postId: string, commentContent: string) {
/** @pact-request-body */
const newComment = {
postId,
commentContent,
};
// ...
}
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.
function fetchComments(/** @pact-query */ query: Query) {
// ...
}
interface Query {
fromUser: string;
postId: string;
}
or
function fetchComments(pageNo: string) {
/** @pact-query */
const params = {
pageNo,
};
// ...
}
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:
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 */
:
interface Address {
city: string;
address: string;
/** @pact-example 99-400 */
postCode: string;
/** @pact-example 45 */
age: number;
}