Skip to content

Commit

Permalink
Added idempotence check for Decider Specification
Browse files Browse the repository at this point in the history
Now you can either pass an empty array or call `thenDoesNothing` to verify if business logic handled idempotency correctly
  • Loading branch information
oskardudycz committed Dec 11, 2024
1 parent d3303a4 commit 946b5b7
Show file tree
Hide file tree
Showing 4 changed files with 127 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ const assertDocumentsEqual = <
expected._id,
actual._id,
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
`Document ids are not matching! Expected: ${expected._id}, actual: ${actual._id}`,
`Document ids are not matching! Expected: ${expected._id}, Actual: ${actual._id}`,
);

return assertDeepEqual(
Expand Down
40 changes: 34 additions & 6 deletions src/packages/emmett/src/testing/assertions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export const assertThrowsAsync = async <TError extends Error>(

assertTrue(
errorCheck(typedError),
`Error doesn't match the expected condition: ${JSON.stringify(error)}`,
`Error doesn't match the expected condition: ${JSONParser.stringify(error)}`,
);

return typedError;
Expand All @@ -65,7 +65,7 @@ export const assertThrows = <TError extends Error>(
if (errorCheck) {
assertTrue(
errorCheck(typedError),
`Error doesn't match the expected condition: ${JSON.stringify(error)}`,
`Error doesn't match the expected condition: ${JSONParser.stringify(error)}`,
);
} else if (typedError instanceof AssertionError) {
assertFalse(
Expand All @@ -79,6 +79,29 @@ export const assertThrows = <TError extends Error>(
throw new AssertionError("Function didn't throw expected error");
};

export const assertDoesNotThrow = <TError extends Error>(
fun: () => void,
errorCheck?: (error: Error) => boolean,
): TError | null => {
try {
fun();
return null;
} catch (error) {
const typedError = error as TError;

if (errorCheck) {
assertFalse(
errorCheck(typedError),
`Error matching the expected condition was thrown!: ${JSONParser.stringify(error)}`,
);
} else {
assertFails(`Function threw an error: ${JSONParser.stringify(error)}`);
}

return typedError;
}
};

export const assertRejects = async <T, TError extends Error = Error>(
promise: Promise<T>,
errorCheck?: ((error: TError) => boolean) | TError,
Expand Down Expand Up @@ -166,7 +189,7 @@ export function assertEqual<T>(
): void {
if (expected !== actual)
throw new AssertionError(
`${message ?? 'Objects are not equal'}:\nExpected: ${JSONParser.stringify(expected)}\nActual:${JSONParser.stringify(actual)}`,
`${message ?? 'Objects are not equal'}:\nExpected: ${JSONParser.stringify(expected)}\nActual: ${JSONParser.stringify(actual)}`,
);
}

Expand Down Expand Up @@ -274,8 +297,13 @@ export function verifyThat(fn: MockedFunction) {

export const assertThatArray = <T>(array: T[]) => {
return {
isEmpty: () => assertEqual(array.length, 0),
isNotEmpty: () => assertNotEqual(array.length, 0),
isEmpty: () =>
assertEqual(
array.length,
0,
`Array is not empty ${JSONParser.stringify(array)}`,
),
isNotEmpty: () => assertNotEqual(array.length, 0, `Array is empty`),
hasSize: (length: number) => assertEqual(array.length, length),
containsElements: (other: T[]) => {
assertTrue(other.every((ts) => array.some((o) => deepEquals(ts, o))));
Expand All @@ -284,7 +312,7 @@ export const assertThatArray = <T>(array: T[]) => {
assertTrue(other.every((ts) => array.some((o) => isSubset(o, ts))));
},
containsOnlyElementsMatching: (other: T[]) => {
assertEqual(array.length, other.length);
assertEqual(array.length, other.length, `Arrays lengths don't match`);
assertTrue(other.every((ts) => array.some((o) => isSubset(o, ts))));
},
containsExactlyInAnyOrder: (other: T[]) => {
Expand Down
16 changes: 14 additions & 2 deletions src/packages/emmett/src/testing/deciderSpecification.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { isErrorConstructor, type ErrorConstructor } from '../errors';
import { AssertionError, assertMatches, assertTrue } from './assertions';
import { AssertionError, assertThatArray, assertTrue } from './assertions';

type ErrorCheck<ErrorType> = (error: ErrorType) => boolean;

Expand All @@ -17,6 +17,7 @@ export type DeciderSpecfication<Command, Event> = (
) => {
when: (command: Command) => {
then: (expectedEvents: Event | Event[]) => void;
thenDoesNothing: () => void;
thenThrows: <ErrorType extends Error = Error>(
...args: Parameters<ThenThrows<ErrorType>>
) => void;
Expand Down Expand Up @@ -58,7 +59,18 @@ export const DeciderSpecification = {
? expectedEvents
: [expectedEvents];

assertMatches(resultEventsArray, expectedEventsArray);
assertThatArray(resultEventsArray).containsOnlyElementsMatching(
expectedEventsArray,
);
},
thenDoesNothing: (): void => {
const resultEvents = handle();

const resultEventsArray = Array.isArray(resultEvents)
? resultEvents
: [resultEvents];

assertThatArray(resultEventsArray).isEmpty();
},
thenThrows: <ErrorType extends Error>(
...args: Parameters<ThenThrows<ErrorType>>
Expand Down
80 changes: 78 additions & 2 deletions src/packages/emmett/src/testing/deciderSpecification.unit.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,14 @@ type Entity = { something: string };
const decide = (
{ data: { something } }: DoSomething,
_entity: Entity,
): SomethingHappened => {
): SomethingHappened | [] | [SomethingHappened] => {
const event: SomethingHappened = { type: 'Did', data: { something } };

if (something === 'Ignore!') return [];
if (something === 'Array!') return [event];
if (something !== 'Yes!') throw new IllegalStateError('Nope!');

return { type: 'Did', data: { something } };
return event;
};

const initialState = (): Entity => ({ something: 'Meh' });
Expand All @@ -30,6 +34,78 @@ const given = DeciderSpecification.for({
});

void describe('DeciderSpecification', () => {
void describe('then', () => {
void it('then fails if returns event, but assertion has an empty array', () => {
assertThrows(
() => {
given([])
.when({
type: 'Do',
data: {
something: 'Yes!',
},
})
.then([]);
},
(error) =>
error instanceof AssertionError &&
error.message ===
`Arrays lengths don't match:\nExpected: 1\nActual: 0`,
);
});
});

void describe('thenDoesNothing', () => {
void it('thenDoesNothing succeeds if returns empty array', () => {
given([])
.when({
type: 'Do',
data: {
something: 'Ignore!',
},
})
.thenDoesNothing();
});

void it('thenDoesNothing fails if returns event', () => {
assertThrows(
() => {
given([])
.when({
type: 'Do',
data: {
something: 'Yes!',
},
})
.thenDoesNothing();
},
(error) =>
error instanceof AssertionError &&
error.message ===
`Array is not empty [{"type":"Did","data":{"something":"Yes!"}}]:\nExpected: 1\nActual: 0`,
);
});

void it('thenDoesNothing fails if returns array of events', () => {
assertThrows(
() => {
given([])
.when({
type: 'Do',
data: {
something: 'Array!',
},
})
.thenDoesNothing();
},
(error) =>
error instanceof AssertionError &&
error.message ===
`Array is not empty [{"type":"Did","data":{"something":"Array!"}}]:\nExpected: 1\nActual: 0`,
);
});
});

void describe('thenThrows', () => {
void it('check error was thrown', () => {
given([])
Expand Down

0 comments on commit 946b5b7

Please sign in to comment.