1 | <?php |
||
2 | |||
3 | declare(strict_types=1); |
||
4 | |||
5 | namespace EventSauce\EventSourcing; |
||
6 | |||
7 | use EventSauce\EventSourcing\TestUtilities\ConsumerThatSerializesMessages; |
||
8 | use EventSauce\EventSourcing\Time\Clock; |
||
9 | use EventSauce\EventSourcing\Time\TestClock; |
||
10 | use Exception; |
||
11 | use LogicException; |
||
12 | use PHPUnit\Framework\TestCase; |
||
13 | use function get_class; |
||
14 | use function method_exists; |
||
15 | use function sprintf; |
||
16 | |||
17 | /** |
||
18 | * @method handle(...$arguments) |
||
19 | */ |
||
20 | abstract class AggregateRootTestCase extends TestCase |
||
21 | { |
||
22 | /** |
||
23 | * @var InMemoryMessageRepository |
||
24 | */ |
||
25 | protected $messageRepository; |
||
26 | |||
27 | /** |
||
28 | * @var AggregateRootRepository |
||
29 | */ |
||
30 | protected $repository; |
||
31 | |||
32 | /** |
||
33 | * @var Exception|null |
||
34 | */ |
||
35 | private $caughtException; |
||
36 | |||
37 | /** |
||
38 | * @var object[] |
||
39 | */ |
||
40 | private $expectedEvents = []; |
||
41 | |||
42 | /** |
||
43 | * @var Exception|null |
||
44 | */ |
||
45 | private $theExpectedException; |
||
46 | |||
47 | /** |
||
48 | * @var TestClock |
||
49 | */ |
||
50 | private $clock; |
||
51 | |||
52 | /** |
||
53 | * @var bool |
||
54 | */ |
||
55 | private $assertedScenario = false; |
||
56 | |||
57 | /** |
||
58 | * @var AggregateRootId |
||
59 | */ |
||
60 | protected $aggregateRootId; |
||
61 | |||
62 | /** |
||
63 | * @before |
||
64 | */ |
||
65 | 10 | protected function setUpEventSauce() |
|
66 | { |
||
67 | 10 | $className = $this->aggregateRootClassName(); |
|
68 | 10 | $this->clock = new TestClock(); |
|
69 | 10 | $this->aggregateRootId = $this->newAggregateRootId(); |
|
70 | 10 | $this->messageRepository = new InMemoryMessageRepository(); |
|
71 | 10 | $dispatcher = $this->messageDispatcher(); |
|
72 | 10 | $decorator = $this->messageDecorator(); |
|
73 | 10 | $this->repository = $this->aggregateRootRepository( |
|
74 | 10 | $className, |
|
75 | 10 | $this->messageRepository, |
|
76 | 10 | $dispatcher, |
|
77 | 10 | $decorator |
|
78 | ); |
||
79 | 10 | $this->expectedEvents = []; |
|
80 | 10 | $this->assertedScenario = false; |
|
81 | 10 | $this->theExpectedException = null; |
|
82 | 10 | $this->caughtException = null; |
|
83 | 10 | } |
|
84 | |||
85 | /** |
||
86 | * @after |
||
87 | */ |
||
88 | 10 | protected function assertScenario() |
|
89 | { |
||
90 | // @codeCoverageIgnoreStart |
||
91 | if ($this->assertedScenario) { |
||
92 | return; |
||
93 | } |
||
94 | // @codeCoverageIgnoreEnd |
||
95 | |||
96 | try { |
||
97 | 10 | $this->assertExpectedException($this->theExpectedException, $this->caughtException); |
|
98 | 7 | $this->assertLastCommitEqualsEvents(...$this->expectedEvents); |
|
99 | 7 | $this->messageRepository->purgeLastCommit(); |
|
100 | 7 | } finally { |
|
101 | 10 | $this->assertedScenario = true; |
|
102 | 10 | $this->theExpectedException = null; |
|
103 | 10 | $this->caughtException = null; |
|
104 | } |
||
105 | 7 | } |
|
106 | |||
107 | 9 | protected function aggregateRootId(): AggregateRootId |
|
108 | { |
||
109 | 9 | return $this->aggregateRootId; |
|
110 | } |
||
111 | |||
112 | abstract protected function newAggregateRootId(): AggregateRootId; |
||
113 | |||
114 | abstract protected function aggregateRootClassName(): string; |
||
115 | |||
116 | /** |
||
117 | * @return $this |
||
118 | */ |
||
119 | 2 | protected function given(object ...$events) |
|
120 | { |
||
121 | 2 | $this->repository->persistEvents($this->aggregateRootId(), 0, ...$events); |
|
122 | 2 | $this->messageRepository->purgeLastCommit(); |
|
123 | |||
124 | 2 | return $this; |
|
125 | } |
||
126 | |||
127 | 1 | public function on(AggregateRootId $id) |
|
128 | { |
||
129 | 1 | return new EventStager($id, $this->messageRepository, $this->repository, $this); |
|
130 | } |
||
131 | |||
132 | /** |
||
133 | * @return $this |
||
134 | */ |
||
135 | 9 | protected function when(...$arguments) |
|
136 | { |
||
137 | try { |
||
138 | 9 | if ( ! method_exists($this, 'handle')) { |
|
139 | 1 | throw new LogicException(sprintf('Class %s is missing a ::handle method.', get_class($this))); |
|
140 | } |
||
141 | |||
142 | 8 | $this->handle(...$arguments); |
|
143 | 4 | } catch (Exception $exception) { |
|
144 | 4 | $this->caughtException = $exception; |
|
145 | } |
||
146 | |||
147 | 9 | return $this; |
|
148 | } |
||
149 | |||
150 | /** |
||
151 | * @return $this |
||
152 | */ |
||
153 | 4 | protected function then(object ...$events) |
|
154 | { |
||
155 | 4 | $this->expectedEvents = $events; |
|
156 | |||
157 | 4 | return $this; |
|
158 | } |
||
159 | |||
160 | /** |
||
161 | * @return $this |
||
162 | */ |
||
163 | 2 | public function expectToFail(Exception $expectedException) |
|
164 | { |
||
165 | 2 | $this->theExpectedException = $expectedException; |
|
166 | |||
167 | 2 | return $this; |
|
168 | } |
||
169 | |||
170 | /** |
||
171 | * @return $this |
||
172 | */ |
||
173 | 1 | protected function thenNothingShouldHaveHappened() |
|
174 | { |
||
175 | 1 | $this->expectedEvents = []; |
|
176 | |||
177 | 1 | return $this; |
|
178 | } |
||
179 | |||
180 | 7 | protected function assertLastCommitEqualsEvents(object ...$events) |
|
181 | { |
||
182 | 7 | self::assertEquals($events, $this->messageRepository->lastCommit(), 'Events are not equal.'); |
|
183 | 7 | } |
|
184 | |||
185 | 10 | private function assertExpectedException(Exception $expectedException = null, Exception $caughtException = null) |
|
186 | { |
||
187 | 10 | if ($expectedException == $caughtException) { |
|
188 | 6 | return; |
|
189 | } |
||
190 | |||
191 | if ( |
||
192 | 4 | $expectedException instanceof Exception |
|
0 ignored issues
–
show
introduced
by
Loading history...
|
|||
193 | 4 | && $caughtException instanceof Exception |
|
194 | 2 | && get_class($expectedException) !== get_class($caughtException) |
|
195 | 4 | || null === $expectedException && $caughtException instanceof Exception |
|
196 | ) { |
||
197 | 3 | throw $caughtException; |
|
198 | } |
||
199 | |||
200 | 1 | self::assertEquals([$expectedException], [$caughtException], '>> Exceptions are not equal.'); |
|
201 | 1 | } |
|
202 | |||
203 | 1 | protected function pointInTime(): PointInTime |
|
204 | { |
||
205 | 1 | return $this->clock->pointInTime(); |
|
206 | } |
||
207 | |||
208 | 8 | protected function clock(): Clock |
|
209 | { |
||
210 | 8 | return $this->clock; |
|
211 | } |
||
212 | |||
213 | 10 | protected function messageDispatcher(): MessageDispatcher |
|
214 | { |
||
215 | 10 | return new SynchronousMessageDispatcher( |
|
216 | 10 | new ConsumerThatSerializesMessages(), |
|
217 | 10 | ...$this->consumers() |
|
218 | ); |
||
219 | } |
||
220 | |||
221 | /** |
||
222 | * @return Consumer[] |
||
223 | */ |
||
224 | 10 | protected function consumers(): array |
|
225 | { |
||
226 | 10 | return []; |
|
227 | } |
||
228 | |||
229 | 10 | private function messageDecorator(): MessageDecorator |
|
230 | { |
||
231 | 10 | return new MessageDecoratorChain(new DefaultHeadersDecorator()); |
|
232 | } |
||
233 | |||
234 | 10 | protected function aggregateRootRepository( |
|
235 | string $className, |
||
236 | MessageRepository $repository, |
||
237 | MessageDispatcher $dispatcher, |
||
238 | MessageDecorator $decorator |
||
239 | ): AggregateRootRepository { |
||
240 | 10 | return new ConstructingAggregateRootRepository( |
|
241 | 10 | $className, |
|
242 | 10 | $repository, |
|
243 | 10 | $dispatcher, |
|
244 | 10 | $decorator |
|
245 | ); |
||
246 | } |
||
247 | } |
||
248 |