Passed
Pull Request — master (#84)
by Frank
14:06 queued 04:07
created

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