Skip to main content

Extending ContractCase

INCOMPLETE DOCUMENT

While ContractCase is in beta, some of the documentation is incomplete or bullet points only.

Each breaking change during the beta, one more document will be completed. If this notice is present in a document, it is not yet considered complete. If you are having trouble using ContractCase or you would like a particular document prioritised, please open an issue

The best way at present to extend case is to submit a PR adding the matcher for the core. There are instructions for adding matchers in the maintainer documentation.

danger

Case is designed to support easy extension, but there is no way currently to tell ContractCase that you have an extension for it. This feature is planned after cross-language support.

Caveats for extending Case

Extensions can be written in any of the languages where ContractCase is available. However, if you wish to distribute your extension for use by others who don't use the same language, it must be written in TypeScript and transpiled with JSii.

Additionally, if your matcher is likely to be of general use, consider making a pull request to add it to the core implementation.

Anatomy of a ContractCase Example

TODO: Describe case example file structure

Extending with a new matcher type

To extend case with a new Matcher type:

  1. Implement the description object. This is the object that will be produced by the DSL, and the content that will be written to the contract file.
  2. Implement the corresponding MatcherExecutor

Designing the description object

All matchers much have a constant for the type of the matcher. The type must have an exported constant for its type. This is used to determine what type of matcher it is and to run the associated matching functions. For example:

export const YOUR_CUSTOM_MATCHER_TYPE = 'yourName:yourMatcher' as const;

Note that all matchers that Case provides are prefixed with case:. To avoid clashing with official matcher names, this prefix is not allowed in extensions.

Export a new interface that describes the actual matcher JSON. This is what will be written to the contract file, and generated by the matcher DSL.

It must include case:matcher:type, set to the exact type constant string you created in the previous step. All parameter fields must be prefixed with case:matcher:. For example:

export interface CoreArrayLengthMatcher {
'case:matcher:type': typeof CORE_ARRAY_LENGTH_MATCHER;
'case:matcher:minLength': number;
'case:matcher:maxLength': number;
}

If your matcher modifies the context object, add fields prefixed with case:context: - these are automatically picked up by ContractCase and rolled into the context before this matcher is invoked. Because case matchers are recursive, this context is passed down to any child matchers.

Implementing the description object

Create a DSL function that creates your matcher type, for example:

/**
* Everything inside this matcher will be matched exactly, unless overridden with an `any*` matcher
*
* Use this to switch out of `shapedLike` and back to the default exact matching.
*
* @param content What
*/
export const exactlyLike = (
content: AnyCaseNodeOrData
): CoreCascadingMatcher => ({
'case:matcher:type': CASCADING_CONTEXT_MATCHER_TYPE,
'case:matcher:child': content,
'case:context:matchBy': 'exact',
});

Implementing the MatcherExecutor

Next, we will add the behaviour of the matcher, both for matching, and for stripping the matchers.

Implement a type that satisfies MatcherExecutor<typeof YOUR_NEW_TYPE_STRING>. For example:

const strip: StripMatcherFn<typeof YOUR_CUSTOM_MATCHER_TYPE> = (
matcher: YourCustomMatcherInterface,
matchContext: MatchContext
): AnyData => // implement the strip matcher function here


const check: CheckMatchFn<typeof YOUR_CUSTOM_MATCHER_TYPE> = (
matcher: YourCustomMatcherInterface,
matchContext: MatchContext,
actual: unknown
): Promise<MatchResult> | MatchResult => // Implement your check here

export const ArrayLengthExecutor: MatcherExecutor<
typeof YOUR_CUSTOM_MATCHER_TYPE
> = { check, strip };

If you need to recurse further into any children of your matchers, use matchContext.descendAndCheck() or matchContext.descendAndStrip() as appropriate. See the existing MatcherExecutor implementations for examples.

If your matcher doesn't have enough context to strip matchers (eg, for auxiliary matchers designed to be used with and()), then throw a new stripUnsupportedError(matcher, matchContext) inside your implementation of strip().

Note that matcher executors are not allowed to call other matcher executors - only descendAndCheck(...). If you need to combine matchers, do it at the DSL layer with and(...)

Extending with a new mock type

To extend case with a new Mock type:

  1. Implement a function that matches the MockSetupFn interface to give ContractCase the behaviour
  2. Implement a function that returns a json-serialisable object that the function you created in the previous step would expect.