Passed
Push — master ( cbefbc...3619f4 )
by Frank
16:49 queued 06:49
created

AggregateRootTestCase::clock()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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