diff --git a/packages/contract-case-dsl-js-jest/src/__tests__/documentation/YourApi.ts b/packages/contract-case-dsl-js-jest/src/__tests__/documentation/YourApi.ts index efd6c5c67..21b1507b9 100644 --- a/packages/contract-case-dsl-js-jest/src/__tests__/documentation/YourApi.ts +++ b/packages/contract-case-dsl-js-jest/src/__tests__/documentation/YourApi.ts @@ -3,6 +3,11 @@ export class YourApi { getUser(_arg0: string): Promise { throw new Error('Method not implemented.'); } + + // eslint-disable-next-line class-methods-use-this + health(): Promise { + throw new Error('Method not implemented.'); + } baseUrl: string; constructor(baseUrl: string) { diff --git a/packages/contract-case-dsl-js-jest/src/__tests__/documentation/defining-a-function-contract-throwing.spec-norun.ts b/packages/contract-case-dsl-js-jest/src/__tests__/documentation/defining-a-function-contract-throwing.spec-norun.ts new file mode 100644 index 000000000..c7a34522b --- /dev/null +++ b/packages/contract-case-dsl-js-jest/src/__tests__/documentation/defining-a-function-contract-throwing.spec-norun.ts @@ -0,0 +1,48 @@ +/* eslint-disable import/no-extraneous-dependencies */ +/* eslint-disable import/order */ + + +import { defineContract, ContractCaseDefiner } from '../../index.js'; + +// example-extract _function-caller-throwing +// The throwing variants don't have convenience functions in +// the Typescript DSL yet, so use the definition class directly +// (from the @contract-case/case-definition-dsl package) +import { interactions } from '@contract-case/case-definition-dsl'; +import { FunctionExecutorConfig } from '@contract-case/contract-case-jest'; + +// ignore-extract +defineContract( + { + consumerName: 'function caller', + providerName: 'function execution', + }, + (contract: ContractCaseDefiner) => { + describe('getUser function', () => { + describe('when the user does not exist', () => { + it('throws UserNotFoundError', async () => { + // end-ignore + await contract.runRejectingInteraction( + { + definition: new interactions.functions.WillCallThrowingFunction({ + arguments: [], + errorClassName: 'UserNotFoundError', + functionName: 'getUser', + }), + }, + { + trigger: async (setup: FunctionExecutorConfig) => + setup.getFunction(setup.mock.functionHandle)(), + // During definition, the mock function throws an Error with a + // message containing the errorClassName defined above + testErrorResponse: (e) => { + expect(e.message).toContain('UserNotFoundError'); + }, + }, + ); + // end-example + }); + }); + }); + }, +); diff --git a/packages/contract-case-dsl-js-jest/src/__tests__/documentation/defining-a-function-contract.spec-norun.ts b/packages/contract-case-dsl-js-jest/src/__tests__/documentation/defining-a-function-contract.spec-norun.ts new file mode 100644 index 000000000..5f72d2f92 --- /dev/null +++ b/packages/contract-case-dsl-js-jest/src/__tests__/documentation/defining-a-function-contract.spec-norun.ts @@ -0,0 +1,50 @@ +/* eslint-disable import/no-extraneous-dependencies */ +/* eslint-disable import/order */ + + +import { defineContract, ContractCaseDefiner } from '../../index.js'; + +// example-extract _function-caller-define +import { + willCallFunction, + FunctionExecutorConfig, +} from '@contract-case/contract-case-jest'; + +// ignore-extract +defineContract( + { + /* The name of the service writing the contract */ + consumerName: 'function caller', + /* The name of the service that will verify the contract */ + providerName: 'function execution', + /* Any additional ContractCaseConfig goes here */ + }, + (contract: ContractCaseDefiner) => { + describe('concatenate function', () => { + it('returns the concatenation of its arguments', async () => { + // end-ignore + await contract.runInteraction( + { + definition: willCallFunction({ + arguments: ['example', 2], + returnValue: 'example2', + functionName: 'concatenate', + }), + }, + { + // The trigger calls the mock function that ContractCase + // has set up for this interaction (see below) + trigger: async (setup: FunctionExecutorConfig) => + setup.getFunction(setup.mock.functionHandle)('example', 2), + // The testResponse function asserts on the value + // returned by the trigger + testResponse: (returnValue) => { + expect(returnValue).toEqual('example2'); + }, + }, + ); + // end-example + }); + }); + }, +); diff --git a/packages/contract-case-dsl-js-jest/src/__tests__/documentation/defining-a-server-contract.spec-norun.ts b/packages/contract-case-dsl-js-jest/src/__tests__/documentation/defining-a-server-contract.spec-norun.ts new file mode 100644 index 000000000..612418277 --- /dev/null +++ b/packages/contract-case-dsl-js-jest/src/__tests__/documentation/defining-a-server-contract.spec-norun.ts @@ -0,0 +1,129 @@ +/* eslint-disable import/no-extraneous-dependencies */ +/* eslint-disable import/order */ +/* eslint-disable jest/expect-expect */ + +import { defineContract } from '../../index.js'; + +import { + inState, + httpStatus, + StateHandlers, + willReceiveHttpRequest, +} from '@contract-case/contract-case-jest'; +import { User } from '../server/entities/responses.js'; + +// Server setup boilerplate - in a real test, you would +// start your own server before the tests run, injecting +// these mocks into its repository layer +const port = 8099; +let mockHealthStatus = true; +let mockGetUser: (id: string) => User | undefined = () => undefined; + +// example-extract _http-server-state-handlers +const stateHandlers: StateHandlers = { + 'Server is up': () => { + mockHealthStatus = true; + }, + 'Server is down': () => { + mockHealthStatus = false; + }, + 'A user exists': { + setup: () => { + const userId = '42'; + mockGetUser = (id) => + id === userId ? { userId, name: 'John' } : undefined; + // Return the userId as a state variable + return { userId }; + }, + teardown: () => { + mockGetUser = () => undefined; + }, + }, +}; +// end-example + +// example-extract _http-server-config +defineContract( + { + consumerName: 'http request consumer', + providerName: 'http request provider', + stateHandlers, + mockConfig: { + http: { + // Replace this with your own server URL + baseUrlUnderTest: `http://localhost:${port}`, + }, + }, + }, + (contract) => { + // ... interaction definitions go here + // ignore-extract + describe('an example placeholder', () => { + it('is never run', () => { + // In a real test, the mock variables above would be wired + // into your running server's repository layer + expect(mockHealthStatus).toBe(true); + expect(mockGetUser('42')).toBeUndefined(); + return contract.runInteraction({ + states: [inState('Server is up')], + definition: willReceiveHttpRequest({ + request: { + method: 'GET', + path: '/health', + }, + response: { status: 200 }, + }), + }); + }); + }); + // end-ignore + }, +); +// end-example + +defineContract( + { + consumerName: 'http request consumer', + providerName: 'http request provider', + stateHandlers, + mockConfig: { + http: { + baseUrlUnderTest: `http://localhost:${port}`, + }, + }, + }, + (contract) => { + // example-extract _http-server-interactions + describe('When the server is up', () => { + const state = inState('Server is up'); + + it('returns a healthy status', () => + contract.runInteraction({ + states: [state], + definition: willReceiveHttpRequest({ + request: { + method: 'GET', + path: '/health', + headers: { accept: 'application/json' }, + }, + response: { status: 200, body: { status: 'up' } }, + }), + })); + }); + + describe('When the server is down', () => { + it('returns an error status', () => + contract.runRejectingInteraction({ + states: [inState('Server is down')], + definition: willReceiveHttpRequest({ + request: { + method: 'GET', + path: '/health', + }, + response: { status: httpStatus(['4XX', '5XX']) }, + }), + })); + }); + // end-example + }, +); diff --git a/packages/contract-case-dsl-js-jest/src/__tests__/documentation/receiving-function-calls-throwing.spec-norun.ts b/packages/contract-case-dsl-js-jest/src/__tests__/documentation/receiving-function-calls-throwing.spec-norun.ts new file mode 100644 index 000000000..ac1cbfa59 --- /dev/null +++ b/packages/contract-case-dsl-js-jest/src/__tests__/documentation/receiving-function-calls-throwing.spec-norun.ts @@ -0,0 +1,41 @@ +/* eslint-disable import/order */ +/* eslint-disable jest/expect-expect */ + +import { defineContract, ContractCaseDefiner } from '../../index.js'; + +// example-extract _function-receiver-throwing +// The throwing variants don't have convenience functions in +// the Typescript DSL yet, so use the definition class directly +// (from the @contract-case/case-definition-dsl package) +import { interactions } from '@contract-case/case-definition-dsl'; + +// ignore-extract +class CustomException extends Error {} + +defineContract( + { + consumerName: 'function execution', + providerName: 'function definer', + }, + (contract: ContractCaseDefiner) => { + describe('throwing function', () => { + it('succeeds', async () => { + // end-ignore + contract.registerFunction('throwingFunction', () => { + throw new CustomException('Oh no'); + }); + + await contract.runInteraction({ + definition: new interactions.functions.WillReceiveFunctionCallAndThrow( + { + arguments: [], + errorClassName: 'CustomException', + functionName: 'throwingFunction', + }, + ), + }); + // end-example + }); + }); + }, +); diff --git a/packages/contract-case-dsl-js-jest/src/__tests__/documentation/receiving-function-calls.spec-norun.ts b/packages/contract-case-dsl-js-jest/src/__tests__/documentation/receiving-function-calls.spec-norun.ts new file mode 100644 index 000000000..c322df76a --- /dev/null +++ b/packages/contract-case-dsl-js-jest/src/__tests__/documentation/receiving-function-calls.spec-norun.ts @@ -0,0 +1,44 @@ +/* eslint-disable import/no-extraneous-dependencies */ +/* eslint-disable import/order */ +/* eslint-disable jest/expect-expect */ + +import { defineContract, ContractCaseDefiner } from '../../index.js'; + +// example-extract _function-receiver-define +import { willReceiveFunctionCall } from '@contract-case/contract-case-jest'; + +// ignore-extract +defineContract( + { + /* The name of the service writing the contract */ + consumerName: 'function execution', + /* The name of the service that will verify the contract */ + providerName: 'function definer', + /* Any additional ContractCaseConfig goes here */ + }, + (contract: ContractCaseDefiner) => { + describe('function with args', () => { + // end-ignore + // This string can be anything you like, as long as it's the same when + // registering the function and when defining the interaction + const FUNCTION_HANDLE = 'HAS ARGS FUNCTION'; + + beforeAll(() => { + contract.registerFunction( + FUNCTION_HANDLE, + (s: string, n: number) => `${s}${n}`, + ); + }); + + it('succeeds', () => + contract.runInteraction({ + definition: willReceiveFunctionCall({ + arguments: ['example', 2], + returnValue: 'example2', + functionName: FUNCTION_HANDLE, + }), + })); + // end-example + }); + }, +); diff --git a/packages/contract-case-dsl-js-jest/src/__tests__/documentation/registering-functions.spec-norun.ts b/packages/contract-case-dsl-js-jest/src/__tests__/documentation/registering-functions.spec-norun.ts new file mode 100644 index 000000000..3d7962636 --- /dev/null +++ b/packages/contract-case-dsl-js-jest/src/__tests__/documentation/registering-functions.spec-norun.ts @@ -0,0 +1,15 @@ +import { verifyContract } from '../../boundaries/jest/jest.js'; + +describe('Function verification', () => { + // example-extract _verify-register-functions + verifyContract( + { + providerName: 'function execution', + }, + (verifier) => { + verifier.registerFunction('zeroArgs', () => {}); + verifier.registerFunction('concatenate', (a, b) => `${a}${b}`); + }, + ); + // end-example +}); diff --git a/packages/contract-case-dsl-js-jest/src/__tests__/documentation/verifying-with-triggers.spec-norun.ts b/packages/contract-case-dsl-js-jest/src/__tests__/documentation/verifying-with-triggers.spec-norun.ts new file mode 100644 index 000000000..27b611780 --- /dev/null +++ b/packages/contract-case-dsl-js-jest/src/__tests__/documentation/verifying-with-triggers.spec-norun.ts @@ -0,0 +1,69 @@ +/* eslint-disable import/no-extraneous-dependencies */ +/* eslint-disable import/order */ + + +import { verifyContract } from '../../boundaries/jest/jest.js'; +import { YourApi } from './YourApi.js'; + +// example-extract _verify-trigger-groups +import { + HttpRequestConfig, + TriggerGroupMap, +} from '@contract-case/contract-case-jest'; + +// ignore-extract +class ApiError extends Error {} + +const api = (baseUrl: string) => new YourApi(baseUrl); + +describe('Contract verification with triggers', () => { + // end-ignore + verifyContract({ + providerName: 'http request provider', + + triggers: new TriggerGroupMap().addTriggerGroup( + 'an http "GET" request to "/health" without a body', + { + trigger: (setup: HttpRequestConfig) => api(setup.mock.baseUrl).health(), + testResponses: { + 'a (200) response with body an object shaped like {status: "up"}': ( + health, + ) => { + expect(health).toEqual('up'); + }, + }, + testErrorResponses: { + 'a (httpStatus 4XX | 5XX) response without a body': (e) => { + expect(e).toBeInstanceOf(ApiError); + }, + }, + }, + ), + }); + // end-example +}); + +describe('Contract verification with state variables', () => { + verifyContract({ + providerName: 'http request provider', + + triggers: new TriggerGroupMap() + // example-extract _verify-trigger-state-variables + .addTriggerGroup( + 'an http "GET" request to "/users/{{userId}}" without a body', + { + trigger: (setup: HttpRequestConfig) => + api(setup.mock.baseUrl).getUser(setup.getStateVariable('userId')), + testResponses: { + 'a (200) response with body an object shaped like {userId: {{userId}}}': + (user, setup) => { + expect(user).toEqual({ + userId: setup.getStateVariable('userId'), + }); + }, + }, + }, + ), + // end-example + }); +}); diff --git a/packages/documentation/docs/defining-contracts/function-calls/defining-example.mdx b/packages/documentation/docs/defining-contracts/function-calls/defining-example.mdx index ee918efbb..84c9c1d9a 100644 --- a/packages/documentation/docs/defining-contracts/function-calls/defining-example.mdx +++ b/packages/documentation/docs/defining-contracts/function-calls/defining-example.mdx @@ -5,6 +5,12 @@ sidebar_position: 3 import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; +import CallerDefineJs from '../../examples/generated/\_function-caller-define.ts.mdx'; +import CallerDefineJava from '../../examples/generated/\_function-caller-define.java.mdx'; + +import CallerThrowingJs from '../../examples/generated/\_function-caller-throwing.ts.mdx'; +import CallerThrowingJava from '../../examples/generated/\_function-caller-throwing.java.mdx'; + # Defining a function call interaction The most common way to define a function call contract is from the caller's @@ -16,51 +22,10 @@ the arguments the caller will use, and the return value it expects: - -```ts -await contract.runInteraction( - { - definition: willCallFunction({ - arguments: ['example', 2], - returnValue: 'example2', - functionName: 'concatenate', - }), - }, - { - // The trigger calls the mock function that ContractCase - // has set up for this interaction (see below) - trigger: async (setup: FunctionExecutorConfig) => - setup.getFunction(setup.mock.functionHandle)('example', 2), - // The testResponse function asserts on the value - // returned by the trigger - testResponse: (returnValue) => { - expect(returnValue).toEqual('example2'); - }, - }, -); -``` - + - -```java -contract.runInteraction( - new InteractionDefinition<>( - List.of(), - WillCallFunction.builder() - .arguments(List.of(new AnyInteger(2))) - .returnValue("2 pages") - .functionName("PageNumbers") - .build()), - IndividualSuccessTestConfigBuilder.builder() - .withTrigger((setupInfo) -> - parse(setupInfo.getFunction(setupInfo.getMockSetup("functionHandle")) - .apply(List.of("2")))) - .withTestResponse((result, setupInfo) -> { - assertThat(result).isEqualTo("2 pages"); - })); -``` - + @@ -103,59 +68,16 @@ same for function call interactions. If your interaction models a call where the function is expected to fail, define it with the throwing variant of the interaction, and pair it with a `testErrorResponse` function (in Typescript, this means using -`runRejectingInteraction`). Instead of a `returnValue`, you describe the error -you expect with `errorClassName` (and, optionally, `message`): +`runRejectingInteraction`; in Java, `runThrowingInteraction`). Instead of a +`returnValue`, you describe the error you expect with `errorClassName` (and, +optionally, `message`): - -```ts -// The throwing variants don't have convenience functions in -// the Typescript DSL yet, so use the definition class directly -// (from the @contract-case/case-definition-dsl package) -import { interactions } from '@contract-case/case-definition-dsl'; - -await contract.runRejectingInteraction( - { - definition: new interactions.functions.WillCallThrowingFunction({ - arguments: [], - errorClassName: 'UserNotFoundError', - functionName: 'getUser', - }), - }, - { - trigger: async (setup: FunctionExecutorConfig) => - setup.getFunction(setup.mock.functionHandle)(), - // During definition, the mock function throws an Error with a - // message containing the errorClassName defined above - testErrorResponse: (e) => { - expect(e.message).toContain('UserNotFoundError'); - }, - }, -); -``` - + - -```java -contract.runInteraction( - new InteractionDefinition<>( - List.of(), - WillCallThrowingFunction.builder() - .arguments(List.of()) - .errorClassName("CustomException") - .functionName("throwingFunction") - .build()), - IndividualFailedTestConfigBuilder.builder() - .withTrigger((setupInfo) -> - setupInfo.getFunction(setupInfo.getMockSetup("functionHandle")) - .apply(List.of())) - .withTestErrorResponse((error, setupInfo) -> { - assertThat(error).isInstanceOf(FunctionCompletedExceptionally.class); - })); -``` - + diff --git a/packages/documentation/docs/defining-contracts/function-calls/receiving-function-calls.mdx b/packages/documentation/docs/defining-contracts/function-calls/receiving-function-calls.mdx index 220a3665f..700d2aabf 100644 --- a/packages/documentation/docs/defining-contracts/function-calls/receiving-function-calls.mdx +++ b/packages/documentation/docs/defining-contracts/function-calls/receiving-function-calls.mdx @@ -5,6 +5,14 @@ sidebar_position: 5 import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; +import ReceiverDefineJs from '../../examples/generated/\_function-receiver-define.ts.mdx'; +import ReceiverDefineJava from '../../examples/generated/\_function-receiver-define.java.mdx'; + +import ReceiverMarshallerJava from '../../examples/generated/\_function-receiver-marshaller.java.mdx'; + +import ReceiverThrowingJs from '../../examples/generated/\_function-receiver-throwing.ts.mdx'; +import ReceiverThrowingJava from '../../examples/generated/\_function-receiver-throwing.java.mdx'; + # Defining from the implementer's side Sometimes it's the function _implementer_ whose expectations should drive the @@ -20,45 +28,10 @@ interaction: - -```ts -// This string can be anything you like, as long as it's the same when -// registering the function and when defining the interaction -const FUNCTION_HANDLE = 'HAS ARGS FUNCTION'; - -beforeAll(() => { - contract.registerFunction( - FUNCTION_HANDLE, - (s: string, n: number) => `${s}${n}`, - ); -}); - -it('succeeds', () => - contract.runInteraction({ - definition: willReceiveFunctionCall({ - arguments: ['example', 2], - returnValue: 'example2', - functionName: FUNCTION_HANDLE, - }), - })); -``` - + - -```java -contract.registerFunction("PageNumbers", convertJsonArgs( - (Integer num) -> num + " pages")); - -contract.runInteraction(new InteractionDefinition<>( - List.of(), - WillReceiveFunctionCall.builder() - .arguments(List.of(new AnyInteger(2))) - .returnValue("2 pages") - .functionName("PageNumbers") - .build())); -``` - + @@ -66,6 +39,21 @@ Because ContractCase generates the caller side itself, there's no `trigger`, `testResponse`, or `testErrorResponse` to write for these interactions. However, because a function contract is defined expecting string arguments, you may need to wrap your function with a marshaller and unmarshaller before providing it to ContractCase. +For example, the `convertJsonArgs` adapter used in the Java example above is: + + + + +``` +// With Typescript/Javascript, arguments and return values +// are marshalled for you, so no adapter is needed +``` + + + + + + ## Functions that throw @@ -75,43 +63,10 @@ error with `errorClassName` instead of a `returnValue`: - -```ts -// The throwing variants don't have convenience functions in -// the Typescript DSL yet, so use the definition class directly -// (from the @contract-case/case-definition-dsl package) -import { interactions } from '@contract-case/case-definition-dsl'; - -contract.registerFunction('throwingFunction', () => { - throw new CustomException('Oh no'); -}); - -await contract.runInteraction({ - definition: new interactions.functions.WillReceiveFunctionCallAndThrow({ - arguments: [], - errorClassName: 'CustomException', - functionName: 'throwingFunction', - }), -}); -``` - + - -```java -contract.registerFunction("throwingFunction", () -> { - throw new CustomException("Oh no"); -}); - -contract.runInteraction(new InteractionDefinition<>( - List.of(), - WillReceiveFunctionCallAndThrow.builder() - .arguments(List.of()) - .errorClassName("CustomException") - .functionName("throwingFunction") - .build())); -``` - + diff --git a/packages/documentation/docs/defining-contracts/http-server/_http-server.mdx b/packages/documentation/docs/defining-contracts/http-server/_http-server.mdx index 7bfeab281..237f211ee 100644 --- a/packages/documentation/docs/defining-contracts/http-server/_http-server.mdx +++ b/packages/documentation/docs/defining-contracts/http-server/_http-server.mdx @@ -18,7 +18,7 @@ using. In large companies, often a server team might publish an API with specific compatibility promises. If possible, it is recommended to -use [client driven contracts](/defining-contracts/http-client) where the client is +use [client driven contracts](../http-client/) where the client is viewed as the consumer. ### What is defined in a server-driven contract? diff --git a/packages/documentation/docs/defining-contracts/http-server/defining-example.mdx b/packages/documentation/docs/defining-contracts/http-server/defining-example.mdx index faf94ffc6..c834352c9 100644 --- a/packages/documentation/docs/defining-contracts/http-server/defining-example.mdx +++ b/packages/documentation/docs/defining-contracts/http-server/defining-example.mdx @@ -2,6 +2,18 @@ sidebar_position: 3 --- +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +import ServerConfigJs from '../../examples/generated/\_http-server-config.ts.mdx'; +import ServerConfigJava from '../../examples/generated/\_http-server-config.java.mdx'; + +import ServerInteractionsJs from '../../examples/generated/\_http-server-interactions.ts.mdx'; +import ServerInteractionsJava from '../../examples/generated/\_http-server-interactions.java.mdx'; + +import ServerStateHandlersJs from '../../examples/generated/\_http-server-state-handlers.ts.mdx'; +import ServerStateHandlersJava from '../../examples/generated/\_http-server-state-handlers.java.mdx'; + # Defining a server-driven interaction Server-driven interactions are defined with `willReceiveHttpRequest`. During @@ -23,65 +35,31 @@ defining an [HTTP client contract](../http-client/): Start your server before the tests run, and tell ContractCase its base URL with the `mockConfig` [configuration option](/docs/reference/configuring): -```ts -defineContract( - { - consumerName: 'http request consumer', - providerName: 'http request provider', - stateHandlers, - mockConfig: { - http: { - // Replace this with your own server URL - baseUrlUnderTest: `http://localhost:${port}`, - }, - }, - }, - (contract) => { - // ... interaction definitions go here - }, -); -``` + + + + + + + + ## Defining an interaction The request and response are described the same way as for HTTP client contracts - only the direction is reversed. `runInteraction` is used for interactions where the client code would succeed, and -`runRejectingInteraction` for interactions where a client would treat the -response as an error: - -```ts -describe('When the server is up', () => { - const state = inState('Server is up'); - - it('returns a healthy status', () => - contract.runInteraction({ - states: [state], - definition: willReceiveHttpRequest({ - request: { - method: 'GET', - path: '/health', - headers: { accept: 'application/json' }, - }, - response: { status: 200, body: { status: 'up' } }, - }), - })); -}); - -describe('When the server is down', () => { - it('returns an error status', () => - contract.runRejectingInteraction({ - states: [inState('Server is down')], - definition: willReceiveHttpRequest({ - request: { - method: 'GET', - path: '/health', - }, - response: { status: httpStatus(['4XX', '5XX']) }, - }), - })); -}); -``` +`runRejectingInteraction` (in Java, `runThrowingInteraction`) for interactions +where a client would treat the response as an error: + + + + + + + + + All the usual [Test Equivalence Matchers](/docs/reference/matchers) are available in both the request and the response. @@ -94,28 +72,14 @@ that put your running server into the named state. These work exactly the same way as state handlers during verification, and the same advice applies: mock the repository layer of your service if you can. -```ts -const stateHandlers: StateHandlers = { - 'Server is up': () => { - mockHealthStatus = true; - }, - 'Server is down': () => { - mockHealthStatus = false; - }, - 'A user exists': { - setup: () => { - const userId = '42'; - mockGetUser = (id) => - id === userId ? { userId, name: 'John' } : undefined; - // Return the userId as a state variable - return { userId }; - }, - teardown: () => { - mockGetUser = () => undefined; - }, - }, -}; -``` + + + + + + + + ### Next steps diff --git a/packages/documentation/docs/examples/generated/_creating-a-contract.java.mdx b/packages/documentation/docs/examples/generated/_creating-a-contract.java.mdx index 91a904acf..03196a2c1 100644 --- a/packages/documentation/docs/examples/generated/_creating-a-contract.java.mdx +++ b/packages/documentation/docs/examples/generated/_creating-a-contract.java.mdx @@ -1,20 +1,19 @@ ```java - private static final ContractDefiner contract = new ContractDefiner( - ContractCaseConfigBuilder.aContractCaseConfig() - .consumerName("Example-Client") - .providerName("Example-Server") - .build()); + private static final ContractDefiner contract = new ContractDefiner( + ContractCaseConfigBuilder.aContractCaseConfig() + .consumerName("Example-Client") + .providerName("Example-Server") + .build()); + public void testSomeApiMethod() { + contract.runInteraction( + /* described later in this chapter */ + ); + } - public void testSomeApiMethod() { - contract.runInteraction( - /* described later in this chapter */ - ); - } - - public void testSomeFailingMethod() { - contract.runThrowingInteraction( - /* described later in this chapter */ - ); - } + public void testSomeFailingMethod() { + contract.runThrowingInteraction( + /* described later in this chapter */ + ); + } ``` diff --git a/packages/documentation/docs/examples/generated/_creating-a-contract.ts.mdx b/packages/documentation/docs/examples/generated/_creating-a-contract.ts.mdx index 59367d16f..1edca66a2 100644 --- a/packages/documentation/docs/examples/generated/_creating-a-contract.ts.mdx +++ b/packages/documentation/docs/examples/generated/_creating-a-contract.ts.mdx @@ -1,8 +1,4 @@ ```ts -import { - ContractCaseDefiner, - defineContract, -} from '@contract-case/contract-case-jest'; defineContract( { diff --git a/packages/documentation/docs/examples/generated/_defining-an-example-config.java.mdx b/packages/documentation/docs/examples/generated/_defining-an-example-config.java.mdx index 63da5404a..115643663 100644 --- a/packages/documentation/docs/examples/generated/_defining-an-example-config.java.mdx +++ b/packages/documentation/docs/examples/generated/_defining-an-example-config.java.mdx @@ -1,16 +1,10 @@ ```java - contract.runInteraction( - new InteractionDefinition<>( - List.of( - /* as above */ - ), - new WillSendHttpRequest( - HttpExample.builder() - /* as above */ - .build()) - ), - IndividualSuccessTestConfigBuilder.builder() - .withLogLevel(LogLevel.DEBUG) - .build() - ); + contract.runInteraction( + new InteractionDefinition<>( + List.of( + /* as above */ + , + /* as above */ + .build()), + IndividualSuccessTestConfigBuilder.builder().withLogLevel(LogLevel.DEBUG).build()); ``` diff --git a/packages/documentation/docs/examples/generated/_defining-an-example-states.java.mdx b/packages/documentation/docs/examples/generated/_defining-an-example-states.java.mdx index 406b164c4..e85db8cef 100644 --- a/packages/documentation/docs/examples/generated/_defining-an-example-states.java.mdx +++ b/packages/documentation/docs/examples/generated/_defining-an-example-states.java.mdx @@ -1,27 +1,16 @@ ```java - contract.runInteraction( - new InteractionDefinition<>( - List.of( - new InState("Server is up"), - new InState("A user with id \"foo\" exists") - ), - new WillSendHttpRequest(HttpExample.builder() - .request( - new HttpRequest(HttpRequestExample.builder() - .method("GET") - .path("/users/foo") - .build()) - ) - .response(new HttpResponse(HttpResponseExample.builder() - .status(200) - .body( - Map.ofEntries( - Map.entry("userId", "foo"), - Map.entry("name", "john smith") - ) - ) - .build())) - .build()) - ), - ); + contract.runInteraction( + new InteractionDefinition<>( + List.of( + new InState("Server is up"), + new InState("A user with id \"foo\" exists")), + WillSendHttpRequest.builder() + .request(HttpRequest.builder().method("GET").path("/users/foo").build()) + .response(HttpResponse.builder() + .status(200) + .body(Map.ofEntries(Map.entry("userId", "foo"), + Map.entry("name", "john smith"))) + .build()) + .build()), + ); ``` diff --git a/packages/documentation/docs/examples/generated/_defining-an-example.java.mdx b/packages/documentation/docs/examples/generated/_defining-an-example.java.mdx index 8fd5eaa4c..b21b9e640 100644 --- a/packages/documentation/docs/examples/generated/_defining-an-example.java.mdx +++ b/packages/documentation/docs/examples/generated/_defining-an-example.java.mdx @@ -1,32 +1,28 @@ ```java - public void testGetUser() { - contract.runInteraction( - new InteractionDefinition<>( - List.of(), // State definitions, covered below - new WillSendHttpRequest(HttpExample.builder() - .request( - new HttpRequest(HttpRequestExample.builder() - .method("GET") - .path("/users/foo") - .build()) - ) - .response(new HttpResponse(HttpResponseExample.builder() - .status(200) - .body( - // Note that we only describe the fields - // that your consumer actually needs for - // this particular test. The real response - // might have more elements, but if your - // consumer doesn't need them, you don't - // need to put them in the contract. - Map.ofEntries( - Map.entry("type", "member"), - Map.entry("name", "john smith") - ) - ) - .build())) - .build()) - ), - ); - } + public void testGetUser() { + contract.runInteraction( + new InteractionDefinition<>( + List.of(), // State definitions, covered below + WillSendHttpRequest.builder() + .request( + HttpRequest.builder() + .method("GET") + .path("/users/foo") + .build()) + .response(HttpResponse.builder() + .status(200) + .body( + // Note that we only describe the fields + // that your consumer actually needs for + // this particular test. The real response + // might have more elements, but if your + // consumer doesn't need them, you don't + // need to put them in the contract. + Map.ofEntries( + Map.entry("type", "member"), + Map.entry("name", "john smith"))) + .build()) + .build()), + ); + } ``` diff --git a/packages/documentation/docs/examples/generated/_defining-states-order.java.mdx b/packages/documentation/docs/examples/generated/_defining-states-order.java.mdx index dc235d81e..be9967c81 100644 --- a/packages/documentation/docs/examples/generated/_defining-states-order.java.mdx +++ b/packages/documentation/docs/examples/generated/_defining-states-order.java.mdx @@ -1,12 +1,10 @@ ```java - contract.runInteraction( - new InteractionDefinition<>( - List.of( - // This one runs first - new InState("Server is up"), - // This one runs second - new InState("A user with id \"foo\" exists") - ), - /* ... */ - ); + contract.runInteraction(new InteractionDefinition<>( + List.of( + // This one runs first + new InState("Server is up"), + // This one runs second + new InState("A user with id \"foo\" exists")), + /* ... */ + ); ``` diff --git a/packages/documentation/docs/examples/generated/_defining-states.java.mdx b/packages/documentation/docs/examples/generated/_defining-states.java.mdx index 10a2f8db6..5f2f7b790 100644 --- a/packages/documentation/docs/examples/generated/_defining-states.java.mdx +++ b/packages/documentation/docs/examples/generated/_defining-states.java.mdx @@ -1,4 +1,4 @@ ```java - new InState("Server is up"), - new InState("A user with id \"foo\" exists") + new InState("Server is up"), + new InState("A user with id \"foo\" exists")), ``` diff --git a/packages/documentation/docs/examples/generated/_end-record.java.mdx b/packages/documentation/docs/examples/generated/_end-record.java.mdx index 431ffea2e..bf979056d 100644 --- a/packages/documentation/docs/examples/generated/_end-record.java.mdx +++ b/packages/documentation/docs/examples/generated/_end-record.java.mdx @@ -1,6 +1,6 @@ ```java - // annotate with @AfterAll if using JUnit - static void after() { - contract.endRecord(); - } + // annotate with @AfterAll if using JUnit + static void after() { + contract.endRecord(); + } ``` diff --git a/packages/documentation/docs/examples/generated/_function-caller-define.java.mdx b/packages/documentation/docs/examples/generated/_function-caller-define.java.mdx new file mode 100644 index 000000000..ef993adca --- /dev/null +++ b/packages/documentation/docs/examples/generated/_function-caller-define.java.mdx @@ -0,0 +1,21 @@ +```java + contract.runInteraction( + new InteractionDefinition<>( + List.of(), + WillCallFunction.builder() + .arguments(List.of(new AnyInteger(2))) + .returnValue("2 pages") + .functionName("PageNumbers") + .build()), + IndividualSuccessTestConfigBuilder.builder() + // The trigger calls the mock function that ContractCase + // has set up for this interaction (see below) + .withTrigger((setupInfo) -> + parse(setupInfo.getFunction(setupInfo.getMockSetup("functionHandle")) + .apply(List.of("2")))) + // The testResponse function asserts on the value + // returned by the trigger + .withTestResponse((result, setupInfo) -> { + assertThat(result).isEqualTo("2 pages"); + })); +``` diff --git a/packages/documentation/docs/examples/generated/_function-caller-define.ts.mdx b/packages/documentation/docs/examples/generated/_function-caller-define.ts.mdx new file mode 100644 index 000000000..a28886460 --- /dev/null +++ b/packages/documentation/docs/examples/generated/_function-caller-define.ts.mdx @@ -0,0 +1,27 @@ +```ts +import { + willCallFunction, + FunctionExecutorConfig, +} from '@contract-case/contract-case-jest'; + + await contract.runInteraction( + { + definition: willCallFunction({ + arguments: ['example', 2], + returnValue: 'example2', + functionName: 'concatenate', + }), + }, + { + // The trigger calls the mock function that ContractCase + // has set up for this interaction (see below) + trigger: async (setup: FunctionExecutorConfig) => + setup.getFunction(setup.mock.functionHandle)('example', 2), + // The testResponse function asserts on the value + // returned by the trigger + testResponse: (returnValue) => { + expect(returnValue).toEqual('example2'); + }, + }, + ); +``` diff --git a/packages/documentation/docs/examples/generated/_function-caller-throwing.java.mdx b/packages/documentation/docs/examples/generated/_function-caller-throwing.java.mdx new file mode 100644 index 000000000..081aef303 --- /dev/null +++ b/packages/documentation/docs/examples/generated/_function-caller-throwing.java.mdx @@ -0,0 +1,19 @@ +```java + contract.runThrowingInteraction( + new InteractionDefinition<>( + List.of(), + WillCallThrowingFunction.builder() + .arguments(List.of()) + .errorClassName("CustomException") + .functionName("throwingFunction") + .build()), + IndividualFailedTestConfigBuilder.builder() + .withTrigger((setupInfo) -> + parse(setupInfo.getFunction(setupInfo.getMockSetup("functionHandle")) + .apply(List.of()))) + // During definition, the mock function throws an exception + // matching the errorClassName defined above + .withTestErrorResponse((exception, setupInfo) -> { + assertThat(exception).isInstanceOf(FunctionCompletedExceptionally.class); + })); +``` diff --git a/packages/documentation/docs/examples/generated/_function-caller-throwing.ts.mdx b/packages/documentation/docs/examples/generated/_function-caller-throwing.ts.mdx new file mode 100644 index 000000000..be7c29e03 --- /dev/null +++ b/packages/documentation/docs/examples/generated/_function-caller-throwing.ts.mdx @@ -0,0 +1,26 @@ +```ts +// The throwing variants don't have convenience functions in +// the Typescript DSL yet, so use the definition class directly +// (from the @contract-case/case-definition-dsl package) +import { interactions } from '@contract-case/case-definition-dsl'; +import { FunctionExecutorConfig } from '@contract-case/contract-case-jest'; + + await contract.runRejectingInteraction( + { + definition: new interactions.functions.WillCallThrowingFunction({ + arguments: [], + errorClassName: 'UserNotFoundError', + functionName: 'getUser', + }), + }, + { + trigger: async (setup: FunctionExecutorConfig) => + setup.getFunction(setup.mock.functionHandle)(), + // During definition, the mock function throws an Error with a + // message containing the errorClassName defined above + testErrorResponse: (e) => { + expect(e.message).toContain('UserNotFoundError'); + }, + }, + ); +``` diff --git a/packages/documentation/docs/examples/generated/_function-receiver-define.java.mdx b/packages/documentation/docs/examples/generated/_function-receiver-define.java.mdx new file mode 100644 index 000000000..3defbe15d --- /dev/null +++ b/packages/documentation/docs/examples/generated/_function-receiver-define.java.mdx @@ -0,0 +1,12 @@ +```java + contract.registerFunction("PageNumbers", convertJsonArgs( + (Integer num) -> num + " pages")); + + contract.runInteraction(new InteractionDefinition<>( + List.of(), + WillReceiveFunctionCall.builder() + .arguments(List.of(new AnyInteger(2))) + .returnValue("2 pages") + .functionName("PageNumbers") + .build())); +``` diff --git a/packages/documentation/docs/examples/generated/_function-receiver-define.ts.mdx b/packages/documentation/docs/examples/generated/_function-receiver-define.ts.mdx new file mode 100644 index 000000000..086ce4101 --- /dev/null +++ b/packages/documentation/docs/examples/generated/_function-receiver-define.ts.mdx @@ -0,0 +1,23 @@ +```ts +import { willReceiveFunctionCall } from '@contract-case/contract-case-jest'; + + // This string can be anything you like, as long as it's the same when + // registering the function and when defining the interaction + const FUNCTION_HANDLE = 'HAS ARGS FUNCTION'; + + beforeAll(() => { + contract.registerFunction( + FUNCTION_HANDLE, + (s: string, n: number) => `${s}${n}`, + ); + }); + + it('succeeds', () => + contract.runInteraction({ + definition: willReceiveFunctionCall({ + arguments: ['example', 2], + returnValue: 'example2', + functionName: FUNCTION_HANDLE, + }), + })); +``` diff --git a/packages/documentation/docs/examples/generated/_function-receiver-marshaller.java.mdx b/packages/documentation/docs/examples/generated/_function-receiver-marshaller.java.mdx new file mode 100644 index 000000000..c07dc1ee9 --- /dev/null +++ b/packages/documentation/docs/examples/generated/_function-receiver-marshaller.java.mdx @@ -0,0 +1,17 @@ +```java + // Because the arguments and return values cross a language boundary, + // registered functions receive JSON strings. A small adapter like this + // parses the arguments and serialises the result of the real function. + @NotNull + private static InvokableFunction1 convertJsonArgs( + Function functionUnderTest) { + return (String a) -> { + try { + var arg1 = mapper.readValue(a, Integer.class); + return mapper.writeValueAsString(functionUnderTest.apply(arg1)); + } catch (JsonProcessingException e) { + throw new RuntimeException("Unable to parse argument"); + } + }; + } +``` diff --git a/packages/documentation/docs/examples/generated/_function-receiver-throwing.java.mdx b/packages/documentation/docs/examples/generated/_function-receiver-throwing.java.mdx new file mode 100644 index 000000000..080530934 --- /dev/null +++ b/packages/documentation/docs/examples/generated/_function-receiver-throwing.java.mdx @@ -0,0 +1,13 @@ +```java + contract.registerFunction("throwingFunction", () -> { + throw new CustomException("Oh no"); + }); + + contract.runInteraction(new InteractionDefinition<>( + List.of(), + WillReceiveFunctionCallAndThrow.builder() + .arguments(List.of()) + .errorClassName("CustomException") + .functionName("throwingFunction") + .build())); +``` diff --git a/packages/documentation/docs/examples/generated/_function-receiver-throwing.ts.mdx b/packages/documentation/docs/examples/generated/_function-receiver-throwing.ts.mdx new file mode 100644 index 000000000..13fea88f9 --- /dev/null +++ b/packages/documentation/docs/examples/generated/_function-receiver-throwing.ts.mdx @@ -0,0 +1,20 @@ +```ts +// The throwing variants don't have convenience functions in +// the Typescript DSL yet, so use the definition class directly +// (from the @contract-case/case-definition-dsl package) +import { interactions } from '@contract-case/case-definition-dsl'; + + contract.registerFunction('throwingFunction', () => { + throw new CustomException('Oh no'); + }); + + await contract.runInteraction({ + definition: new interactions.functions.WillReceiveFunctionCallAndThrow( + { + arguments: [], + errorClassName: 'CustomException', + functionName: 'throwingFunction', + }, + ), + }); +``` diff --git a/packages/documentation/docs/examples/generated/_http-server-config.java.mdx b/packages/documentation/docs/examples/generated/_http-server-config.java.mdx new file mode 100644 index 000000000..d431de9bf --- /dev/null +++ b/packages/documentation/docs/examples/generated/_http-server-config.java.mdx @@ -0,0 +1,11 @@ +```java + private static final ContractDefiner contract = new ContractDefiner( + ContractCaseConfigBuilder.aContractCaseConfig() + .consumerName("http request consumer") + .providerName("http request provider") + .mockConfig("http", Map.of( + // Replace this with your own server URL + "baseUrlUnderTest", "http://localhost:" + port)) + // State handlers are described below + .build()); +``` diff --git a/packages/documentation/docs/examples/generated/_http-server-config.ts.mdx b/packages/documentation/docs/examples/generated/_http-server-config.ts.mdx new file mode 100644 index 000000000..e5eb14c3f --- /dev/null +++ b/packages/documentation/docs/examples/generated/_http-server-config.ts.mdx @@ -0,0 +1,18 @@ +```ts +defineContract( + { + consumerName: 'http request consumer', + providerName: 'http request provider', + stateHandlers, + mockConfig: { + http: { + // Replace this with your own server URL + baseUrlUnderTest: `http://localhost:${port}`, + }, + }, + }, + (contract) => { + // ... interaction definitions go here + }, +); +``` diff --git a/packages/documentation/docs/examples/generated/_http-server-interactions.java.mdx b/packages/documentation/docs/examples/generated/_http-server-interactions.java.mdx new file mode 100644 index 000000000..42bee8cae --- /dev/null +++ b/packages/documentation/docs/examples/generated/_http-server-interactions.java.mdx @@ -0,0 +1,29 @@ +```java + contract.runInteraction( + new InteractionDefinition<>( + List.of(new InState("Server is up")), + WillReceiveHttpRequest.builder() + .request(HttpRequest.builder() + .method("GET") + .path("/health") + .headers(Map.of("accept", "application/json")) + .build()) + .response(HttpResponse.builder() + .status(200) + .body(Map.of("status", "up")) + .build()) + .build())); + + contract.runThrowingInteraction( + new InteractionDefinition<>( + List.of(new InState("Server is down")), + WillReceiveHttpRequest.builder() + .request(HttpRequest.builder() + .method("GET") + .path("/health") + .build()) + .response(HttpResponse.builder() + .status(new HttpStatusCodes(List.of("4XX", "5XX"))) + .build()) + .build())); +``` diff --git a/packages/documentation/docs/examples/generated/_http-server-interactions.ts.mdx b/packages/documentation/docs/examples/generated/_http-server-interactions.ts.mdx new file mode 100644 index 000000000..c29f333e7 --- /dev/null +++ b/packages/documentation/docs/examples/generated/_http-server-interactions.ts.mdx @@ -0,0 +1,32 @@ +```ts + describe('When the server is up', () => { + const state = inState('Server is up'); + + it('returns a healthy status', () => + contract.runInteraction({ + states: [state], + definition: willReceiveHttpRequest({ + request: { + method: 'GET', + path: '/health', + headers: { accept: 'application/json' }, + }, + response: { status: 200, body: { status: 'up' } }, + }), + })); + }); + + describe('When the server is down', () => { + it('returns an error status', () => + contract.runRejectingInteraction({ + states: [inState('Server is down')], + definition: willReceiveHttpRequest({ + request: { + method: 'GET', + path: '/health', + }, + response: { status: httpStatus(['4XX', '5XX']) }, + }), + })); + }); +``` diff --git a/packages/documentation/docs/examples/generated/_http-server-state-handlers.java.mdx b/packages/documentation/docs/examples/generated/_http-server-state-handlers.java.mdx new file mode 100644 index 000000000..6f627b754 --- /dev/null +++ b/packages/documentation/docs/examples/generated/_http-server-state-handlers.java.mdx @@ -0,0 +1,26 @@ +```java + ContractCaseConfigBuilder.aContractCaseConfig() + .stateHandler( + "Server is up", + StateHandler.setupFunction(() -> { + // Put your server into the 'Server is up' state here, + // for example by mocking the repository layer + })) + .stateHandler( + "Server is down", + StateHandler.setupFunction(() -> { + // Put your server into the 'Server is down' state here + })) + .stateHandler( + "A user exists", + StateHandler.setupAndTeardown( + () -> { + // Set up the user in your server's repository layer, + // then return the userId as a state variable + return Map.of("userId", "42"); + }, + () -> { + // Remove the mock, so that the server state + // is the same as it was before the test + })) +``` diff --git a/packages/documentation/docs/examples/generated/_http-server-state-handlers.ts.mdx b/packages/documentation/docs/examples/generated/_http-server-state-handlers.ts.mdx new file mode 100644 index 000000000..47e9b7512 --- /dev/null +++ b/packages/documentation/docs/examples/generated/_http-server-state-handlers.ts.mdx @@ -0,0 +1,22 @@ +```ts +const stateHandlers: StateHandlers = { + 'Server is up': () => { + mockHealthStatus = true; + }, + 'Server is down': () => { + mockHealthStatus = false; + }, + 'A user exists': { + setup: () => { + const userId = '42'; + mockGetUser = (id) => + id === userId ? { userId, name: 'John' } : undefined; + // Return the userId as a state variable + return { userId }; + }, + teardown: () => { + mockGetUser = () => undefined; + }, + }, +}; +``` diff --git a/packages/documentation/docs/examples/generated/_matchers-intro.java.mdx b/packages/documentation/docs/examples/generated/_matchers-intro.java.mdx index 4a5190b2f..85b800466 100644 --- a/packages/documentation/docs/examples/generated/_matchers-intro.java.mdx +++ b/packages/documentation/docs/examples/generated/_matchers-intro.java.mdx @@ -1,12 +1,7 @@ ```java - HttpResponseExample.builder() - .status(200) - .body( - Map.ofEntries( - Map.entry("userId", "foo"), - // You can read this as "this test covers any 'name' property - // which is a string (for example, 'john smith'). - Map.entry("name", new AnyString("john smith")) - ) - ) + HttpResponse.builder().status(200).body(Map.ofEntries( + Map.entry("userId", "foo"), + // You can read this as + // "this test covers any 'name' that is a string (for example, 'john smith')". + Map.entry("name", new AnyString("john smith")))).build() ``` diff --git a/packages/documentation/docs/examples/generated/_matchers-state-no-vars.java.mdx b/packages/documentation/docs/examples/generated/_matchers-state-no-vars.java.mdx index 73c204fbb..fed20c771 100644 --- a/packages/documentation/docs/examples/generated/_matchers-state-no-vars.java.mdx +++ b/packages/documentation/docs/examples/generated/_matchers-state-no-vars.java.mdx @@ -1,3 +1,3 @@ ```java - new InState("A user with id \"foo\" exists"), + new InState("A user with id \"foo\" exists"), ``` diff --git a/packages/documentation/docs/examples/generated/_matchers-state-vars-complete.java.mdx b/packages/documentation/docs/examples/generated/_matchers-state-vars-complete.java.mdx index c602ea309..4a7689009 100644 --- a/packages/documentation/docs/examples/generated/_matchers-state-vars-complete.java.mdx +++ b/packages/documentation/docs/examples/generated/_matchers-state-vars-complete.java.mdx @@ -1,49 +1,40 @@ ```java - contract.runInteraction( - new InteractionDefinition<>( - List.of( - new InStateWithVariables( - "A user exists", - Map.ofEntries( - Map.entry("userId", "123") - ) - ) - ), - new WillSendHttpRequest(HttpExample.builder() - .request( - new HttpRequest(HttpRequestExample.builder() - .method("GET") - .path("/users") - // The StateVariable matcher tells ContractCase that - // the id in the query will be the userId from the - // state setup. - .query(Map.of("id", new StateVariable("userId"))) - .build()) - ) - .response(new HttpResponse(HttpResponseExample.builder() - .status(200) - .body( - Map.ofEntries( - // In the response body, we expect the - // userId to be the same as the one set up - // during state setup - Map.entry("id", new StateVariable("userId")), - // and the name may be any non-empty string - // (but during the contract definition, it will be "John Smith") - Map.entry("name", new AnyString("john smith")) - ) - ) - .build())) - .build()) - ), - IndividualSuccessTestConfigBuilder.builder() - .withTrigger( - (interactionSetup) -> - new YourApiClient(interactionSetup.getMockSetup("baseUrl")) - .getUserQuery(interactionSetup.getStateVariable("userId"))) - .withTestResponse( - (user, config) -> { - assertThat(user).isEqualTo(new User("foo", "")); - }) - ); + contract.runInteraction( + new InteractionDefinition<>( + List.of( + new InStateWithVariables( + "A user exists", + Map.ofEntries( + Map.entry("userId", "123")))), + WillSendHttpRequest.builder() + .request( + HttpRequest.builder() + .method("GET") + .path("/users") + // The StateVariable matcher tells ContractCase that + // the id in the query will be the userId from the + // state setup. + .query(Map.of("id", new StateVariable("userId"))) + .build()) + .response(HttpResponse.builder() + .status(200) + .body( + Map.ofEntries( + // In the response body, we expect the + // userId to be the same as the one set up + // during state setup + Map.entry("id", new StateVariable("userId")), + // and the name may be any non-empty string + // (but during the contract definition, it will be "John Smith") + Map.entry("name", new AnyString("john smith")))) + .build()) + .build()), + IndividualSuccessTestConfigBuilder.builder() + .withTrigger( + (interactionSetup) -> new YourApiClient(interactionSetup.getMockSetup("baseUrl")) + .getUserQuery(interactionSetup.getStringStateVariable("userId"))) + .withTestResponse( + (user, config) -> { + assertThat(user).isEqualTo(new User("foo", "", List.of())); + })); ``` diff --git a/packages/documentation/docs/examples/generated/_matchers-state-with-vars.java.mdx b/packages/documentation/docs/examples/generated/_matchers-state-with-vars.java.mdx index 89252899d..cb601873e 100644 --- a/packages/documentation/docs/examples/generated/_matchers-state-with-vars.java.mdx +++ b/packages/documentation/docs/examples/generated/_matchers-state-with-vars.java.mdx @@ -1,8 +1,6 @@ ```java - new InStateWithVariables( - "A user exists", - Map.ofEntries( - Map.entry("userId", "123") - ) - ), + new InStateWithVariables( + "A user exists", + Map.ofEntries( + Map.entry("userId", "123"))), ``` diff --git a/packages/documentation/docs/examples/generated/_state-matchers.java.mdx b/packages/documentation/docs/examples/generated/_state-matchers.java.mdx index 2c475f382..c5f0af67e 100644 --- a/packages/documentation/docs/examples/generated/_state-matchers.java.mdx +++ b/packages/documentation/docs/examples/generated/_state-matchers.java.mdx @@ -1,37 +1,19 @@ ```java - contract.runInteraction( - new InteractionDefinition<>( - List.of( - new InState("Server is up"), - new InStateWithVariables( - "A user exists", - Map.of("userId", "123") - ) - ), - new WillSendHttpRequest( - HttpExample.builder() - .request( - new HttpRequest(HttpRequestExample.builder() - .method("GET") - .path( - new StringPrefix( - "/users", - new StateVariable("userId") - ) - ).build() - ) - ) - .response(new HttpResponse(HttpResponseExample.builder() - .status(200) - .body( - Map.ofEntries( - Map.entry("userId", new StateVariable("userId")), - Map.entry("name", new AnyString("john smith")) - ) - ) - .build())) - .build()) - ), - /* ... */ - ); + contract.runInteraction( + new InteractionDefinition<>(List.of( + new InState("Server is up"), + new InStateWithVariables("A user exists", Map.of("userId", "123"))), + WillSendHttpRequest.builder() + .request(HttpRequest.builder() + .method("GET") + .path(new StringPrefix("/users", new StateVariable("userId"))) + .build()) + .response(HttpResponse.builder() + .status(200) + .body(Map.ofEntries(Map.entry("userId", new StateVariable("userId")), + Map.entry("name", new AnyString("john smith")))) + .build()) + .build()), + /* ... */ + ); ``` diff --git a/packages/documentation/docs/examples/generated/_state-variables.java.mdx b/packages/documentation/docs/examples/generated/_state-variables.java.mdx index 2a5e35d3d..9a50bfe29 100644 --- a/packages/documentation/docs/examples/generated/_state-variables.java.mdx +++ b/packages/documentation/docs/examples/generated/_state-variables.java.mdx @@ -1,3 +1,3 @@ ```java - new InStateWithVariables("A user exists", Map.of("userId", "foo")); + new InStateWithVariables("A user exists", Map.of("userId", "foo")); ``` diff --git a/packages/documentation/docs/examples/generated/_trigger-http-client.java.mdx b/packages/documentation/docs/examples/generated/_trigger-http-client.java.mdx index e599a7723..51b356d89 100644 --- a/packages/documentation/docs/examples/generated/_trigger-http-client.java.mdx +++ b/packages/documentation/docs/examples/generated/_trigger-http-client.java.mdx @@ -1,11 +1,7 @@ ```java - IndividualSuccessTestConfigBuilder.builder() - .withTrigger( - (interactionSetup) -> - new YourApiClient( - interactionSetup.getMockSetup("baseUrl") - ).getUserQuery( - interactionSetup.getStateVariable("userId") - ) - ) + IndividualSuccessTestConfigBuilder.builder() + .withTrigger( + (interactionSetup) -> new YourApiClient( + interactionSetup.getMockSetup("baseUrl")).getUserQuery( + interactionSetup.getStringStateVariable("userId"))) ``` diff --git a/packages/documentation/docs/examples/generated/_verify-register-functions.java.mdx b/packages/documentation/docs/examples/generated/_verify-register-functions.java.mdx new file mode 100644 index 000000000..f88bcc3c3 --- /dev/null +++ b/packages/documentation/docs/examples/generated/_verify-register-functions.java.mdx @@ -0,0 +1,14 @@ +```java + ContractVerifier verifier = new ContractVerifier( + ContractCaseConfigBuilder.aContractCaseConfig() + .providerName("Java Function Implementer Example") + .build()); + + verifier.registerFunction("NoArgFunction", () -> null); + verifier.registerFunction( + "PageNumbers", + convertJsonIntegerArg((Integer num) -> num + " pages")); + + verifier.runVerification( + ContractCaseConfigBuilder.aContractCaseConfig().build()); +``` diff --git a/packages/documentation/docs/examples/generated/_verify-register-functions.ts.mdx b/packages/documentation/docs/examples/generated/_verify-register-functions.ts.mdx new file mode 100644 index 000000000..c2a174820 --- /dev/null +++ b/packages/documentation/docs/examples/generated/_verify-register-functions.ts.mdx @@ -0,0 +1,11 @@ +```ts + verifyContract( + { + providerName: 'function execution', + }, + (verifier) => { + verifier.registerFunction('zeroArgs', () => {}); + verifier.registerFunction('concatenate', (a, b) => `${a}${b}`); + }, + ); +``` diff --git a/packages/documentation/docs/examples/generated/_verify-register-marshaller.java.mdx b/packages/documentation/docs/examples/generated/_verify-register-marshaller.java.mdx new file mode 100644 index 000000000..241cf1ca0 --- /dev/null +++ b/packages/documentation/docs/examples/generated/_verify-register-marshaller.java.mdx @@ -0,0 +1,16 @@ +```java + // Registered functions receive each argument as a JSON string, and + // must return a JSON string. An adapter like this parses the arguments + // and serialises the result of the real function under test. + private static @NotNull InvokableFunction1 + convertJsonIntegerArg(Function functionUnderTest) { + return (String a) -> { + try { + var arg1 = mapper.readValue(a, Integer.class); + return mapper.writeValueAsString(functionUnderTest.apply(arg1)); + } catch (JsonProcessingException e) { + throw new RuntimeException("Unable to parse argument"); + } + }; + } +``` diff --git a/packages/documentation/docs/examples/generated/_verify-trigger-groups.java.mdx b/packages/documentation/docs/examples/generated/_verify-trigger-groups.java.mdx new file mode 100644 index 000000000..bee825ed9 --- /dev/null +++ b/packages/documentation/docs/examples/generated/_verify-trigger-groups.java.mdx @@ -0,0 +1,34 @@ +```java + // The trigger invokes your real client code against the mock server + Trigger getHealth = (setupInfo) -> { + try { + return new YourApiClient(setupInfo.getMockSetup("baseUrl")).getHealth(); + } catch (IOException e) { + throw new RuntimeException(e); + } + }; + + contract.runVerification(ContractCaseConfigBuilder.aContractCaseConfig() + .triggers(new TriggerGroups() + .addTriggerGroup(new TriggerGroup<>( + "an http \"GET\" request to \"/health\" without a body", + getHealth, + // Test functions for interactions where the client + // code is expected to succeed, keyed by the + // response description + Map.of( + "a (200) response with body an object shaped like {status: \"up\"}", + (String result, InteractionSetup setupInfo) -> { + assertThat(result).isEqualTo("up"); + }), + // Test functions for interactions where the client + // code is expected to throw, keyed by the + // response description + Map.of( + "a (httpStatus 4XX | 5XX) response without a body", + (Exception exception, InteractionSetup setupInfo) -> { + assertThat(exception.getMessage()) + .isEqualTo("The server is not ready"); + })))) + .build()); +``` diff --git a/packages/documentation/docs/examples/generated/_verify-trigger-groups.ts.mdx b/packages/documentation/docs/examples/generated/_verify-trigger-groups.ts.mdx new file mode 100644 index 000000000..da4db5379 --- /dev/null +++ b/packages/documentation/docs/examples/generated/_verify-trigger-groups.ts.mdx @@ -0,0 +1,29 @@ +```ts +import { + HttpRequestConfig, + TriggerGroupMap, +} from '@contract-case/contract-case-jest'; + + verifyContract({ + providerName: 'http request provider', + + triggers: new TriggerGroupMap().addTriggerGroup( + 'an http "GET" request to "/health" without a body', + { + trigger: (setup: HttpRequestConfig) => api(setup.mock.baseUrl).health(), + testResponses: { + 'a (200) response with body an object shaped like {status: "up"}': ( + health, + ) => { + expect(health).toEqual('up'); + }, + }, + testErrorResponses: { + 'a (httpStatus 4XX | 5XX) response without a body': (e) => { + expect(e).toBeInstanceOf(ApiError); + }, + }, + }, + ), + }); +``` diff --git a/packages/documentation/docs/examples/generated/_verify-trigger-state-variables.java.mdx b/packages/documentation/docs/examples/generated/_verify-trigger-state-variables.java.mdx new file mode 100644 index 000000000..5ecdfc561 --- /dev/null +++ b/packages/documentation/docs/examples/generated/_verify-trigger-state-variables.java.mdx @@ -0,0 +1,24 @@ +```java + Trigger getUser = (setupInfo) -> { + try { + return new YourApiClient(setupInfo.getMockSetup("baseUrl")) + .getUser(setupInfo.getStateVariable("userId")); + } catch (IOException e) { + throw new RuntimeException(e); + } + }; + + contract.runVerification(ContractCaseConfigBuilder.aContractCaseConfig() + .triggers(new TriggerGroups() + .addTriggerGroup(new TriggerGroup<>( + "an http \"GET\" request to \"/users/{{userId}}\" without a body", + getUser, + Map.of( + "a (200) response with body an object shaped like {userId: {{userId}}}", + (User user, InteractionSetup setupInfo) -> { + assertThat(user.userId()) + .isEqualTo(setupInfo.getStateVariable("userId")); + }), + Map.of()))) + .build()); +``` diff --git a/packages/documentation/docs/examples/generated/_verify-trigger-state-variables.ts.mdx b/packages/documentation/docs/examples/generated/_verify-trigger-state-variables.ts.mdx new file mode 100644 index 000000000..a4971555b --- /dev/null +++ b/packages/documentation/docs/examples/generated/_verify-trigger-state-variables.ts.mdx @@ -0,0 +1,17 @@ +```ts + .addTriggerGroup( + 'an http "GET" request to "/users/{{userId}}" without a body', + { + trigger: (setup: HttpRequestConfig) => + api(setup.mock.baseUrl).getUser(setup.getStateVariable('userId')), + testResponses: { + 'a (200) response with body an object shaped like {userId: {{userId}}}': + (user, setup) => { + expect(user).toEqual({ + userId: setup.getStateVariable('userId'), + }); + }, + }, + }, + ), +``` diff --git a/packages/documentation/docs/examples/generated/_verifying-state-handlers.java.mdx b/packages/documentation/docs/examples/generated/_verifying-state-handlers.java.mdx index e02fc2ae9..761f830b1 100644 --- a/packages/documentation/docs/examples/generated/_verifying-state-handlers.java.mdx +++ b/packages/documentation/docs/examples/generated/_verifying-state-handlers.java.mdx @@ -1,36 +1,45 @@ ```java - contract.runVerification( - ContractCaseConfigBuilder.aContractCaseConfig() - // State handlers are keyed by the name of the state. - // This must match exactly between the name defined in the - // contract, and the state handler at verification time. + try { + var tests = contract.prepareVerification( + ContractCaseConfigBuilder.aContractCaseConfig() + // State handlers are keyed by the name of the state. + // This must match exactly between the name defined in the + // contract, and the state handler at verification time. - // A state handler either returns void, or variables. - // - // They can be created with the factories on StateHandler: - // - // StateHandler.setupFunction(() -> void) - // StateHandler.setupFunction(() -> Map) - // StateHandler.setupAndTearDown(() -> Map, () -> void) - // - // If your state returns variables, return an object where the - // keys are the variable names instead of void. - .stateHandler( - "Server is up", - StateHandler.setupFunction(() -> { - // Any setup for the state 'Server is up' goes here - }) - ).stateHandler( - "A user exists", - StateHandler.setupAndTeardown( - () -> { - // Any setup for the state 'A user exists' goes here - }, - () -> { - // Any teardown for the state 'A user exists' goes here - } - ) - ) - .build() - ); + // A state handler either returns void, or variables. + // + // They can be created with the factories on StateHandler: + // + // StateHandler.setupFunction(() -> void) + // StateHandler.setupFunction(() -> Map) + // StateHandler.setupAndTearDown(() -> Map, () -> void) + // + // If your state returns variables, return an object where the + // keys are the variable names instead of void. + .stateHandler( + "Server is up", + StateHandler.setupFunction(() -> { + // Any setup for the state 'Server is up' goes here + }) + ).stateHandler( + "A user exists", + StateHandler.setupAndTeardown( + () -> { + // Any setup for the state 'A user exists' goes here + }, + () -> { + // Any teardown for the state 'A user exists' goes here + } + ) + ) + .build() + ); + + for (var test : tests) { + // This runs each test one by one + contract.runPreparedTest(test); + } + } finally { + contract.close(); + } ``` diff --git a/packages/documentation/docs/examples/generated/_verifying-state-handlers.ts.mdx b/packages/documentation/docs/examples/generated/_verifying-state-handlers.ts.mdx index 4cacaa015..183b0d908 100644 --- a/packages/documentation/docs/examples/generated/_verifying-state-handlers.ts.mdx +++ b/packages/documentation/docs/examples/generated/_verifying-state-handlers.ts.mdx @@ -1,37 +1,35 @@ ```ts - verifier.runVerification({ - stateHandlers: { - // State handlers are keyed by the name of the state. - // This must match exactly between the name defined in the - // contract, and the state handler at verification time. + stateHandlers: { + // State handlers are keyed by the name of the state. + // This must match exactly between the name defined in the + // contract, and the state handler at verification time. - // A state handler either returns void, or variables. - // - // It generally has the type: - // { - // setup: () => Promise | void - // teardown: () => Promise | void - // } - // - // If you only need a setup handler, you can use: - // - // () => Promise | void - // - // instead. - // - // If your state returns variables, return an object where the - // keys are the variable names instead of void. - 'Server is up': () => { - // Any setup for the state 'Server is up' goes here - }, - 'A user exists': { - setup: () => { - // Any setup for the state 'A user exists' goes here - }, - teardown: () => { - // Any teardown for the state 'A user exists' goes here - }, - }, + // A state handler either returns void, or variables. + // + // It generally has the type: + // { + // setup: () => Promise | void + // teardown: () => Promise | void + // } + // + // If you only need a setup handler, you can use: + // + // () => Promise | void + // + // instead. + // + // If your state returns variables, return an object where the + // keys are the variable names instead of void. + 'Server is up': () => { + // Any setup for the state 'Server is up' goes here + }, + 'A user exists': { + setup: () => { + // Any setup for the state 'A user exists' goes here }, - }), + teardown: () => { + // Any teardown for the state 'A user exists' goes here + }, + }, + }, ``` diff --git a/packages/documentation/docs/verifying-contracts/function-execution/registering-functions.mdx b/packages/documentation/docs/verifying-contracts/function-execution/registering-functions.mdx index 1082ee42a..653e74030 100644 --- a/packages/documentation/docs/verifying-contracts/function-execution/registering-functions.mdx +++ b/packages/documentation/docs/verifying-contracts/function-execution/registering-functions.mdx @@ -5,6 +5,11 @@ sidebar_position: 3 import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; +import RegisterFunctionsJs from '../../examples/generated/\_verify-register-functions.ts.mdx'; +import RegisterFunctionsJava from '../../examples/generated/\_verify-register-functions.java.mdx'; + +import RegisterMarshallerJava from '../../examples/generated/\_verify-register-marshaller.java.mdx'; + # Registering functions During verification, ContractCase needs to be able to invoke the real @@ -18,37 +23,10 @@ the second is the implementation to invoke: - -```ts -verifyContract( - { - providerName: 'function execution', - }, - (verifier) => { - verifier.registerFunction('zeroArgs', () => {}); - verifier.registerFunction('concatenate', (a, b) => `${a}${b}`); - }, -); -``` - + - -```java -ContractVerifier verifier = new ContractVerifier( - ContractCaseConfigBuilder.aContractCaseConfig() - .providerName("Java Function Implementer Example") - .build()); - -verifier.registerFunction("NoArgFunction", () -> null); -verifier.registerFunction( - "PageNumbers", - convertJsonIntegerArg((Integer num) -> num + " pages")); - -verifier.runVerification( - ContractCaseConfigBuilder.aContractCaseConfig().build()); -``` - + @@ -62,7 +40,22 @@ Arguments and return values are JSON-serialised when they cross the boundary between ContractCase and your code. In dynamically typed languages like typescript, this is handled for you. In Java, registered functions receive each argument as a JSON string and must return a JSON string, so you may need a small adapter around -the function under test to parse the arguments and serialise the result. +the function under test to parse the arguments and serialise the result. For +example, the `convertJsonIntegerArg` adapter used above is: + + + + +``` +// With Typescript/Javascript, arguments and return values +// are marshalled for you, so no adapter is needed +``` + + + + + + ## Functions that throw diff --git a/packages/documentation/docs/verifying-contracts/http-client/triggers.mdx b/packages/documentation/docs/verifying-contracts/http-client/triggers.mdx index e527d8c8b..be5da352a 100644 --- a/packages/documentation/docs/verifying-contracts/http-client/triggers.mdx +++ b/packages/documentation/docs/verifying-contracts/http-client/triggers.mdx @@ -2,6 +2,15 @@ sidebar_position: 3 --- +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +import TriggerGroupsJs from '../../examples/generated/\_verify-trigger-groups.ts.mdx'; +import TriggerGroupsJava from '../../examples/generated/\_verify-trigger-groups.java.mdx'; + +import TriggerStateVariablesJs from '../../examples/generated/\_verify-trigger-state-variables.ts.mdx'; +import TriggerStateVariablesJava from '../../examples/generated/\_verify-trigger-state-variables.java.mdx'; + # Providing triggers for verification During verification of a server-driven contract, ContractCase doesn't know how @@ -10,32 +19,17 @@ in the contract: the trigger that invokes your real client code, plus the test functions that assert on what your client returned (or threw). Trigger groups are provided with the `triggers` [configuration -option](/docs/reference/configuring), built with a `TriggerGroupMap`: - -```ts -verifyContract({ - providerName: 'http request provider', - - triggers: new TriggerGroupMap().addTriggerGroup( - 'an http "GET" request to "/health" without a body', - { - trigger: (setup: HttpRequestConfig) => api(setup.mock.baseUrl).health(), - testResponses: { - 'a (200) response with body an object shaped like {status: "up"}': ( - health, - ) => { - expect(health).toEqual('up'); - }, - }, - testErrorResponses: { - 'a (httpStatus 4XX | 5XX) response without a body': (e) => { - expect(e).toBeInstanceOf(ApiError); - }, - }, - }, - ), -}); -``` +option](/docs/reference/configuring), built with a `TriggerGroupMap` (in Java, +`TriggerGroups`): + + + + + + + + + Each trigger group contains: @@ -69,20 +63,14 @@ variables](../../defining-contracts/http-client/state-definitions), the resolved values are available through `setup.getStateVariable(...)` in both the trigger and the test functions: -```ts -.addTriggerGroup('an http "GET" request to "/users/{{userId}}" without a body', { - trigger: (setup: HttpRequestConfig) => - api(setup.mock.baseUrl).getUser(setup.getStateVariable('userId')), - testResponses: { - 'a (200) response with body an object shaped like {userId: {{userId}}}': ( - user, - setup, - ) => { - expect(user).toEqual({ userId: setup.getStateVariable('userId') }); - }, - }, -}) -``` + + + + + + + + During verification of a server-driven contract, state handlers aren't run (the mock server plays back the recorded responses), so state variables take diff --git a/packages/dsl-java/src/test/java/io/contract_testing/contractcase/documentation/DefiningAFunctionContract.java b/packages/dsl-java/src/test/java/io/contract_testing/contractcase/documentation/DefiningAFunctionContract.java new file mode 100644 index 000000000..27839b977 --- /dev/null +++ b/packages/dsl-java/src/test/java/io/contract_testing/contractcase/documentation/DefiningAFunctionContract.java @@ -0,0 +1,79 @@ +package io.contract_testing.contractcase.documentation; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.contract_testing.contractcase.ContractDefiner; +import io.contract_testing.contractcase.InteractionDefinition; +import io.contract_testing.contractcase.configuration.ContractCaseConfig.ContractCaseConfigBuilder; +import io.contract_testing.contractcase.configuration.IndividualFailedTestConfig.IndividualFailedTestConfigBuilder; +import io.contract_testing.contractcase.configuration.IndividualSuccessTestConfig.IndividualSuccessTestConfigBuilder; +import io.contract_testing.contractcase.dsl.interactions.functions.WillCallFunction; +import io.contract_testing.contractcase.dsl.interactions.functions.WillCallThrowingFunction; +import io.contract_testing.contractcase.dsl.matchers.primitives.AnyInteger; +import io.contract_testing.contractcase.exceptions.FunctionCompletedExceptionally; +import java.util.List; + +public class DefiningAFunctionContract { + + private static final ContractDefiner contract = new ContractDefiner( + ContractCaseConfigBuilder.aContractCaseConfig() + .consumerName("Java Function Caller Example") + .providerName("Java Function Implementer Example") + .build()); + + public void testCallFunction() { + // example-extract _function-caller-define + contract.runInteraction( + new InteractionDefinition<>( + List.of(), + WillCallFunction.builder() + .arguments(List.of(new AnyInteger(2))) + .returnValue("2 pages") + .functionName("PageNumbers") + .build()), + IndividualSuccessTestConfigBuilder.builder() + // The trigger calls the mock function that ContractCase + // has set up for this interaction (see below) + .withTrigger((setupInfo) -> + parse(setupInfo.getFunction(setupInfo.getMockSetup("functionHandle")) + .apply(List.of("2")))) + // The testResponse function asserts on the value + // returned by the trigger + .withTestResponse((result, setupInfo) -> { + assertThat(result).isEqualTo("2 pages"); + })); + // end-example + } + + public void testThrowingFunction() { + // example-extract _function-caller-throwing + contract.runThrowingInteraction( + new InteractionDefinition<>( + List.of(), + WillCallThrowingFunction.builder() + .arguments(List.of()) + .errorClassName("CustomException") + .functionName("throwingFunction") + .build()), + IndividualFailedTestConfigBuilder.builder() + .withTrigger((setupInfo) -> + parse(setupInfo.getFunction(setupInfo.getMockSetup("functionHandle")) + .apply(List.of()))) + // During definition, the mock function throws an exception + // matching the errorClassName defined above + .withTestErrorResponse((exception, setupInfo) -> { + assertThat(exception).isInstanceOf(FunctionCompletedExceptionally.class); + })); + // end-example + } + + private String parse(String json) { + try { + return new ObjectMapper().readValue(json, String.class); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } +} diff --git a/packages/dsl-java/src/test/java/io/contract_testing/contractcase/documentation/DefiningAServerContract.java b/packages/dsl-java/src/test/java/io/contract_testing/contractcase/documentation/DefiningAServerContract.java new file mode 100644 index 000000000..3c44e5ddf --- /dev/null +++ b/packages/dsl-java/src/test/java/io/contract_testing/contractcase/documentation/DefiningAServerContract.java @@ -0,0 +1,98 @@ +package io.contract_testing.contractcase.documentation; + +import io.contract_testing.contractcase.ContractDefiner; +import io.contract_testing.contractcase.InteractionDefinition; +import io.contract_testing.contractcase.configuration.ContractCaseConfig.ContractCaseConfigBuilder; +import io.contract_testing.contractcase.configuration.StateHandler; +import io.contract_testing.contractcase.dsl.interactions.http.WillReceiveHttpRequest; +import io.contract_testing.contractcase.dsl.matchers.http.HttpRequest; +import io.contract_testing.contractcase.dsl.matchers.http.HttpResponse; +import io.contract_testing.contractcase.dsl.matchers.http.HttpStatusCodes; +import io.contract_testing.contractcase.dsl.states.InState; +import java.util.List; +import java.util.Map; + +public class DefiningAServerContract { + + private static final int port = 8099; + + // example-extract _http-server-config + private static final ContractDefiner contract = new ContractDefiner( + ContractCaseConfigBuilder.aContractCaseConfig() + .consumerName("http request consumer") + .providerName("http request provider") + .mockConfig("http", Map.of( + // Replace this with your own server URL + "baseUrlUnderTest", "http://localhost:" + port)) + // State handlers are described below + // ignore-extract + .stateHandler( + "Server is up", + StateHandler.setupFunction(() -> { + })) + // end-ignore + .build()); + // end-example + + public void testStateHandlers() { + // example-extract _http-server-state-handlers + ContractCaseConfigBuilder.aContractCaseConfig() + .stateHandler( + "Server is up", + StateHandler.setupFunction(() -> { + // Put your server into the 'Server is up' state here, + // for example by mocking the repository layer + })) + .stateHandler( + "Server is down", + StateHandler.setupFunction(() -> { + // Put your server into the 'Server is down' state here + })) + .stateHandler( + "A user exists", + StateHandler.setupAndTeardown( + () -> { + // Set up the user in your server's repository layer, + // then return the userId as a state variable + return Map.of("userId", "42"); + }, + () -> { + // Remove the mock, so that the server state + // is the same as it was before the test + })) + // end-example + .build(); + } + + public void testServerInteractions() { + // example-extract _http-server-interactions + contract.runInteraction( + new InteractionDefinition<>( + List.of(new InState("Server is up")), + WillReceiveHttpRequest.builder() + .request(HttpRequest.builder() + .method("GET") + .path("/health") + .headers(Map.of("accept", "application/json")) + .build()) + .response(HttpResponse.builder() + .status(200) + .body(Map.of("status", "up")) + .build()) + .build())); + + contract.runThrowingInteraction( + new InteractionDefinition<>( + List.of(new InState("Server is down")), + WillReceiveHttpRequest.builder() + .request(HttpRequest.builder() + .method("GET") + .path("/health") + .build()) + .response(HttpResponse.builder() + .status(new HttpStatusCodes(List.of("4XX", "5XX"))) + .build()) + .build())); + // end-example + } +} diff --git a/packages/dsl-java/src/test/java/io/contract_testing/contractcase/documentation/ReceivingFunctionCalls.java b/packages/dsl-java/src/test/java/io/contract_testing/contractcase/documentation/ReceivingFunctionCalls.java new file mode 100644 index 000000000..687bc61a4 --- /dev/null +++ b/packages/dsl-java/src/test/java/io/contract_testing/contractcase/documentation/ReceivingFunctionCalls.java @@ -0,0 +1,75 @@ +package io.contract_testing.contractcase.documentation; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.contract_testing.contractcase.ContractDefiner; +import io.contract_testing.contractcase.InteractionDefinition; +import io.contract_testing.contractcase.configuration.ContractCaseConfig.ContractCaseConfigBuilder; +import io.contract_testing.contractcase.configuration.InvokableFunctions.InvokableFunction1; +import io.contract_testing.contractcase.dsl.interactions.functions.WillReceiveFunctionCall; +import io.contract_testing.contractcase.dsl.interactions.functions.WillReceiveFunctionCallAndThrow; +import io.contract_testing.contractcase.dsl.matchers.primitives.AnyInteger; +import io.contract_testing.contractcase.test.function.verification.CustomException; +import java.util.List; +import java.util.function.Function; +import org.jetbrains.annotations.NotNull; + +public class ReceivingFunctionCalls { + + static final ObjectMapper mapper = new ObjectMapper(); + + private static final ContractDefiner contract = new ContractDefiner( + ContractCaseConfigBuilder.aContractCaseConfig() + .consumerName("Java Function Implementer Example") + .providerName("Java Function Caller Example") + .build()); + + public void testReceiveFunctionCall() { + // example-extract _function-receiver-define + contract.registerFunction("PageNumbers", convertJsonArgs( + (Integer num) -> num + " pages")); + + contract.runInteraction(new InteractionDefinition<>( + List.of(), + WillReceiveFunctionCall.builder() + .arguments(List.of(new AnyInteger(2))) + .returnValue("2 pages") + .functionName("PageNumbers") + .build())); + // end-example + } + + public void testReceiveThrowingFunctionCall() { + // example-extract _function-receiver-throwing + contract.registerFunction("throwingFunction", () -> { + throw new CustomException("Oh no"); + }); + + contract.runInteraction(new InteractionDefinition<>( + List.of(), + WillReceiveFunctionCallAndThrow.builder() + .arguments(List.of()) + .errorClassName("CustomException") + .functionName("throwingFunction") + .build())); + // end-example + } + + // example-extract _function-receiver-marshaller + // Because the arguments and return values cross a language boundary, + // registered functions receive JSON strings. A small adapter like this + // parses the arguments and serialises the result of the real function. + @NotNull + private static InvokableFunction1 convertJsonArgs( + Function functionUnderTest) { + return (String a) -> { + try { + var arg1 = mapper.readValue(a, Integer.class); + return mapper.writeValueAsString(functionUnderTest.apply(arg1)); + } catch (JsonProcessingException e) { + throw new RuntimeException("Unable to parse argument"); + } + }; + } + // end-example +} diff --git a/packages/dsl-java/src/test/java/io/contract_testing/contractcase/documentation/RegisteringFunctions.java b/packages/dsl-java/src/test/java/io/contract_testing/contractcase/documentation/RegisteringFunctions.java new file mode 100644 index 000000000..24540a21b --- /dev/null +++ b/packages/dsl-java/src/test/java/io/contract_testing/contractcase/documentation/RegisteringFunctions.java @@ -0,0 +1,48 @@ +package io.contract_testing.contractcase.documentation; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.contract_testing.contractcase.ContractVerifier; +import io.contract_testing.contractcase.configuration.ContractCaseConfig.ContractCaseConfigBuilder; +import io.contract_testing.contractcase.configuration.InvokableFunctions.InvokableFunction1; +import java.util.function.Function; +import org.jetbrains.annotations.NotNull; + +public class RegisteringFunctions { + + static final ObjectMapper mapper = new ObjectMapper(); + + public void testRegisterFunctions() { + // example-extract _verify-register-functions + ContractVerifier verifier = new ContractVerifier( + ContractCaseConfigBuilder.aContractCaseConfig() + .providerName("Java Function Implementer Example") + .build()); + + verifier.registerFunction("NoArgFunction", () -> null); + verifier.registerFunction( + "PageNumbers", + convertJsonIntegerArg((Integer num) -> num + " pages")); + + verifier.runVerification( + ContractCaseConfigBuilder.aContractCaseConfig().build()); + // end-example + } + + // example-extract _verify-register-marshaller + // Registered functions receive each argument as a JSON string, and + // must return a JSON string. An adapter like this parses the arguments + // and serialises the result of the real function under test. + private static @NotNull InvokableFunction1 + convertJsonIntegerArg(Function functionUnderTest) { + return (String a) -> { + try { + var arg1 = mapper.readValue(a, Integer.class); + return mapper.writeValueAsString(functionUnderTest.apply(arg1)); + } catch (JsonProcessingException e) { + throw new RuntimeException("Unable to parse argument"); + } + }; + } + // end-example +} diff --git a/packages/dsl-java/src/test/java/io/contract_testing/contractcase/documentation/VerifyingWithTriggers.java b/packages/dsl-java/src/test/java/io/contract_testing/contractcase/documentation/VerifyingWithTriggers.java new file mode 100644 index 000000000..04b0744fd --- /dev/null +++ b/packages/dsl-java/src/test/java/io/contract_testing/contractcase/documentation/VerifyingWithTriggers.java @@ -0,0 +1,86 @@ +package io.contract_testing.contractcase.documentation; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.contract_testing.contractcase.ContractVerifier; +import io.contract_testing.contractcase.configuration.ContractCaseConfig.ContractCaseConfigBuilder; +import io.contract_testing.contractcase.configuration.InteractionSetup; +import io.contract_testing.contractcase.configuration.Trigger; +import io.contract_testing.contractcase.configuration.TriggerGroup; +import io.contract_testing.contractcase.configuration.TriggerGroups; +import io.contract_testing.contractcase.test.httpclient.implementation.User; +import io.contract_testing.contractcase.test.httpclient.implementation.YourApiClient; +import java.io.IOException; +import java.util.Map; + +public class VerifyingWithTriggers { + + private static final ContractVerifier contract = new ContractVerifier( + ContractCaseConfigBuilder.aContractCaseConfig() + .providerName("http request provider") + .build()); + + public void testVerifyWithTriggers() throws InterruptedException { + // example-extract _verify-trigger-groups + // The trigger invokes your real client code against the mock server + Trigger getHealth = (setupInfo) -> { + try { + return new YourApiClient(setupInfo.getMockSetup("baseUrl")).getHealth(); + } catch (IOException e) { + throw new RuntimeException(e); + } + }; + + contract.runVerification(ContractCaseConfigBuilder.aContractCaseConfig() + .triggers(new TriggerGroups() + .addTriggerGroup(new TriggerGroup<>( + "an http \"GET\" request to \"/health\" without a body", + getHealth, + // Test functions for interactions where the client + // code is expected to succeed, keyed by the + // response description + Map.of( + "a (200) response with body an object shaped like {status: \"up\"}", + (String result, InteractionSetup setupInfo) -> { + assertThat(result).isEqualTo("up"); + }), + // Test functions for interactions where the client + // code is expected to throw, keyed by the + // response description + Map.of( + "a (httpStatus 4XX | 5XX) response without a body", + (Exception exception, InteractionSetup setupInfo) -> { + assertThat(exception.getMessage()) + .isEqualTo("The server is not ready"); + })))) + .build()); + // end-example + } + + public void testVerifyWithStateVariables() throws InterruptedException { + // example-extract _verify-trigger-state-variables + Trigger getUser = (setupInfo) -> { + try { + return new YourApiClient(setupInfo.getMockSetup("baseUrl")) + .getUser(setupInfo.getStateVariable("userId")); + } catch (IOException e) { + throw new RuntimeException(e); + } + }; + + contract.runVerification(ContractCaseConfigBuilder.aContractCaseConfig() + .triggers(new TriggerGroups() + .addTriggerGroup(new TriggerGroup<>( + "an http \"GET\" request to \"/users/{{userId}}\" without a body", + getUser, + Map.of( + "a (200) response with body an object shaped like {userId: {{userId}}}", + (User user, InteractionSetup setupInfo) -> { + assertThat(user.userId()) + .isEqualTo(setupInfo.getStateVariable("userId")); + }), + Map.of()))) + .build()); + // end-example + } +}