Skip to content

Commit 946b5b7

Browse files
committed
Added idempotence check for Decider Specification
Now you can either pass an empty array or call `thenDoesNothing` to verify if business logic handled idempotency correctly
1 parent d3303a4 commit 946b5b7

File tree

4 files changed

+127
-11
lines changed

4 files changed

+127
-11
lines changed

src/packages/emmett-postgresql/src/eventStore/projections/pongo/pongoProjectionSpec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ const assertDocumentsEqual = <
6161
expected._id,
6262
actual._id,
6363
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
64-
`Document ids are not matching! Expected: ${expected._id}, actual: ${actual._id}`,
64+
`Document ids are not matching! Expected: ${expected._id}, Actual: ${actual._id}`,
6565
);
6666

6767
return assertDeepEqual(

src/packages/emmett/src/testing/assertions.ts

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ export const assertThrowsAsync = async <TError extends Error>(
4545

4646
assertTrue(
4747
errorCheck(typedError),
48-
`Error doesn't match the expected condition: ${JSON.stringify(error)}`,
48+
`Error doesn't match the expected condition: ${JSONParser.stringify(error)}`,
4949
);
5050

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

82+
export const assertDoesNotThrow = <TError extends Error>(
83+
fun: () => void,
84+
errorCheck?: (error: Error) => boolean,
85+
): TError | null => {
86+
try {
87+
fun();
88+
return null;
89+
} catch (error) {
90+
const typedError = error as TError;
91+
92+
if (errorCheck) {
93+
assertFalse(
94+
errorCheck(typedError),
95+
`Error matching the expected condition was thrown!: ${JSONParser.stringify(error)}`,
96+
);
97+
} else {
98+
assertFails(`Function threw an error: ${JSONParser.stringify(error)}`);
99+
}
100+
101+
return typedError;
102+
}
103+
};
104+
82105
export const assertRejects = async <T, TError extends Error = Error>(
83106
promise: Promise<T>,
84107
errorCheck?: ((error: TError) => boolean) | TError,
@@ -166,7 +189,7 @@ export function assertEqual<T>(
166189
): void {
167190
if (expected !== actual)
168191
throw new AssertionError(
169-
`${message ?? 'Objects are not equal'}:\nExpected: ${JSONParser.stringify(expected)}\nActual:${JSONParser.stringify(actual)}`,
192+
`${message ?? 'Objects are not equal'}:\nExpected: ${JSONParser.stringify(expected)}\nActual: ${JSONParser.stringify(actual)}`,
170193
);
171194
}
172195

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

275298
export const assertThatArray = <T>(array: T[]) => {
276299
return {
277-
isEmpty: () => assertEqual(array.length, 0),
278-
isNotEmpty: () => assertNotEqual(array.length, 0),
300+
isEmpty: () =>
301+
assertEqual(
302+
array.length,
303+
0,
304+
`Array is not empty ${JSONParser.stringify(array)}`,
305+
),
306+
isNotEmpty: () => assertNotEqual(array.length, 0, `Array is empty`),
279307
hasSize: (length: number) => assertEqual(array.length, length),
280308
containsElements: (other: T[]) => {
281309
assertTrue(other.every((ts) => array.some((o) => deepEquals(ts, o))));
@@ -284,7 +312,7 @@ export const assertThatArray = <T>(array: T[]) => {
284312
assertTrue(other.every((ts) => array.some((o) => isSubset(o, ts))));
285313
},
286314
containsOnlyElementsMatching: (other: T[]) => {
287-
assertEqual(array.length, other.length);
315+
assertEqual(array.length, other.length, `Arrays lengths don't match`);
288316
assertTrue(other.every((ts) => array.some((o) => isSubset(o, ts))));
289317
},
290318
containsExactlyInAnyOrder: (other: T[]) => {

src/packages/emmett/src/testing/deciderSpecification.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { isErrorConstructor, type ErrorConstructor } from '../errors';
2-
import { AssertionError, assertMatches, assertTrue } from './assertions';
2+
import { AssertionError, assertThatArray, assertTrue } from './assertions';
33

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

@@ -17,6 +17,7 @@ export type DeciderSpecfication<Command, Event> = (
1717
) => {
1818
when: (command: Command) => {
1919
then: (expectedEvents: Event | Event[]) => void;
20+
thenDoesNothing: () => void;
2021
thenThrows: <ErrorType extends Error = Error>(
2122
...args: Parameters<ThenThrows<ErrorType>>
2223
) => void;
@@ -58,7 +59,18 @@ export const DeciderSpecification = {
5859
? expectedEvents
5960
: [expectedEvents];
6061

61-
assertMatches(resultEventsArray, expectedEventsArray);
62+
assertThatArray(resultEventsArray).containsOnlyElementsMatching(
63+
expectedEventsArray,
64+
);
65+
},
66+
thenDoesNothing: (): void => {
67+
const resultEvents = handle();
68+
69+
const resultEventsArray = Array.isArray(resultEvents)
70+
? resultEvents
71+
: [resultEvents];
72+
73+
assertThatArray(resultEventsArray).isEmpty();
6274
},
6375
thenThrows: <ErrorType extends Error>(
6476
...args: Parameters<ThenThrows<ErrorType>>

src/packages/emmett/src/testing/deciderSpecification.unit.spec.ts

Lines changed: 78 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,14 @@ type Entity = { something: string };
1111
const decide = (
1212
{ data: { something } }: DoSomething,
1313
_entity: Entity,
14-
): SomethingHappened => {
14+
): SomethingHappened | [] | [SomethingHappened] => {
15+
const event: SomethingHappened = { type: 'Did', data: { something } };
16+
17+
if (something === 'Ignore!') return [];
18+
if (something === 'Array!') return [event];
1519
if (something !== 'Yes!') throw new IllegalStateError('Nope!');
1620

17-
return { type: 'Did', data: { something } };
21+
return event;
1822
};
1923

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

3236
void describe('DeciderSpecification', () => {
37+
void describe('then', () => {
38+
void it('then fails if returns event, but assertion has an empty array', () => {
39+
assertThrows(
40+
() => {
41+
given([])
42+
.when({
43+
type: 'Do',
44+
data: {
45+
something: 'Yes!',
46+
},
47+
})
48+
.then([]);
49+
},
50+
(error) =>
51+
error instanceof AssertionError &&
52+
error.message ===
53+
`Arrays lengths don't match:\nExpected: 1\nActual: 0`,
54+
);
55+
});
56+
});
57+
58+
void describe('thenDoesNothing', () => {
59+
void it('thenDoesNothing succeeds if returns empty array', () => {
60+
given([])
61+
.when({
62+
type: 'Do',
63+
data: {
64+
something: 'Ignore!',
65+
},
66+
})
67+
.thenDoesNothing();
68+
});
69+
70+
void it('thenDoesNothing fails if returns event', () => {
71+
assertThrows(
72+
() => {
73+
given([])
74+
.when({
75+
type: 'Do',
76+
data: {
77+
something: 'Yes!',
78+
},
79+
})
80+
.thenDoesNothing();
81+
},
82+
(error) =>
83+
error instanceof AssertionError &&
84+
error.message ===
85+
`Array is not empty [{"type":"Did","data":{"something":"Yes!"}}]:\nExpected: 1\nActual: 0`,
86+
);
87+
});
88+
89+
void it('thenDoesNothing fails if returns array of events', () => {
90+
assertThrows(
91+
() => {
92+
given([])
93+
.when({
94+
type: 'Do',
95+
data: {
96+
something: 'Array!',
97+
},
98+
})
99+
.thenDoesNothing();
100+
},
101+
(error) =>
102+
error instanceof AssertionError &&
103+
error.message ===
104+
`Array is not empty [{"type":"Did","data":{"something":"Array!"}}]:\nExpected: 1\nActual: 0`,
105+
);
106+
});
107+
});
108+
33109
void describe('thenThrows', () => {
34110
void it('check error was thrown', () => {
35111
given([])

0 commit comments

Comments
 (0)