Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ export class YourApi {
getUser(_arg0: string): Promise<unknown> {
throw new Error('Method not implemented.');
}

// eslint-disable-next-line class-methods-use-this
health(): Promise<string> {
throw new Error('Method not implemented.');
}
baseUrl: string;

constructor(baseUrl: string) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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
});
});
});
},
);
Original file line number Diff line number Diff line change
@@ -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
});
});
},
);
Original file line number Diff line number Diff line change
@@ -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
},
);
Original file line number Diff line number Diff line change
@@ -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
});
});
},
);
Original file line number Diff line number Diff line change
@@ -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
});
},
);
Original file line number Diff line number Diff line change
@@ -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
});
Loading
Loading