Passed
Pull Request — master (#84)
by Frank
12:21 queued 02:23
created

AggregateRootTestCase::expectToFail()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 5
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
    public function on(AggregateRootId $id): EventStager
154
    {
155
        return new EventStager($id, $this->messageRepository, $this->repository, $this);
156
    }
157
158
    /**
159
     * @param mixed[] $arguments
160
     *
161
     * @return $this
162
     */
163
    protected function when(...$arguments)
164
    {
165
        try {
166
            if ( ! method_exists($this, 'handle')) {
167
                throw new LogicException(sprintf('Class %s is missing a ::handle method.', get_class($this)));
168
            }
169
170
            $this->handle(...$arguments);
171
        } catch (Exception $exception) {
172
            $this->caughtException = $exception;
173
        }
174
175
        return $this;
176
    }
177
178
    /**
179
     * @return $this
180
     */
181
    protected function then(object ...$events)
182
    {
183
        $this->expectedEvents = $events;
184
185
        return $this;
186
    }
187
188
    /**
189
     * @return $this
190
     */
191
    public function expectToFail(Exception $expectedException)
192
    {
193
        $this->theExpectedException = $expectedException;
194
195
        return $this;
196
    }
197
198
    /**
199
     * @return $this
200
     */
201
    protected function thenNothingShouldHaveHappened()
202
    {
203
        $this->expectedEvents = [];
204
205
        return $this;
206
    }
207
208
    protected function assertLastCommitEqualsEvents(object ...$events): void
209
    {
210
        self::assertEquals($events, $this->messageRepository->lastCommit(), 'Events are not equal.');
211
    }
212
213
    private function assertExpectedException(
214
        Exception $expectedException = null,
215
        Exception $caughtException = null
216
    ): void {
217
        if (null !== $caughtException && (null === $expectedException || get_class($expectedException) !== get_class(
218
                    $caughtException
219
                ))) {
220
            throw $caughtException;
221
        }
222
223
        self::assertEquals([$expectedException], [$caughtException], '>> Exceptions are not equal.');
224
    }
225
226
    protected function currentTime(): DateTimeImmutable
227
    {
228
        return $this->clock->now();
229
    }
230
231
    protected function clock(): TestClock
232
    {
233
        return $this->clock;
234
    }
235
236
    protected function messageDispatcher(): MessageDispatcher
237
    {
238
        return new SynchronousMessageDispatcher(
239
            new MessageConsumerThatSerializesMessages(), ...$this->consumers()
240
        );
241
    }
242
243
    /**
244
     * @return MessageConsumer[]
245
     */
246
    protected function consumers(): array
247
    {
248
        return [];
249
    }
250
251
    private function messageDecorator(): MessageDecorator
252
    {
253
        return new MessageDecoratorChain(new DefaultHeadersDecorator());
254
    }
255
256
    /**
257
     * @template T of AggregateRoot
258
     *
259
     * @phpstan-param class-string<T> $className
260
     *
261
     * @phpstan-return AggregateRootRepository<T>
262
     */
263
    protected function aggregateRootRepository(
264
        string $className,
265
        MessageRepository $repository,
266
        MessageDispatcher $dispatcher,
267
        MessageDecorator $decorator
268
    ): AggregateRootRepository {
269
        return new ConstructingAggregateRootRepository(
270
            $className, $repository, $dispatcher, $decorator
271
        );
272
    }
273
}
274