Passed
Pull Request — master (#84)
by Frank
07:12
created

assertLastCommitEqualsEvents()   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 1
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 EventSauce\EventSourcing\AggregateRoot;
8
use EventSauce\EventSourcing\AggregateRootId;
9
use EventSauce\EventSourcing\AggregateRootRepository;
10
use EventSauce\EventSourcing\ConstructingAggregateRootRepository;
11
use EventSauce\EventSourcing\DefaultHeadersDecorator;
12
use EventSauce\EventSourcing\InMemoryMessageRepository;
13
use EventSauce\EventSourcing\MessageConsumer;
14
use EventSauce\EventSourcing\MessageDecorator;
15
use EventSauce\EventSourcing\MessageDecoratorChain;
16
use EventSauce\EventSourcing\MessageDispatcher;
17
use EventSauce\EventSourcing\MessageRepository;
18
use EventSauce\EventSourcing\PointInTime;
19
use EventSauce\EventSourcing\SynchronousMessageDispatcher;
20
use EventSauce\EventSourcing\Time\Clock;
21
use EventSauce\EventSourcing\Time\TestClock;
22
use Exception;
23
use LogicException;
24
use PHPUnit\Framework\TestCase;
25
use function get_class;
26
use function method_exists;
27
use function sprintf;
28
29
/**
30
 * @method handle(...$arguments)
31
 */
32
abstract class AggregateRootTestCase extends TestCase
33
{
34
    /**
35
     * @var InMemoryMessageRepository
36
     */
37
    protected $messageRepository;
38
39
    /**
40
     * @var AggregateRootRepository
41
     */
42
    protected $repository;
43
44
    /**
45
     * @var Exception|null
46
     */
47
    private $caughtException;
48
49
    /**
50
     * @var object[]
51
     */
52
    private $expectedEvents = [];
53
54
    /**
55
     * @var Exception|null
56
     */
57
    private $theExpectedException;
58
59
    /**
60
     * @var TestClock
61
     */
62
    private $clock;
63
64
    /**
65
     * @var bool
66
     */
67
    private $assertedScenario = false;
68
69
    /**
70
     * @var AggregateRootId
71
     */
72
    protected $aggregateRootId;
73
74
    /**
75
     * @before
76
     */
77
    protected function setUpEventSauce(): void
78
    {
79
        $className = $this->aggregateRootClassName();
80
        $this->clock = new TestClock();
81
        $this->aggregateRootId = $this->newAggregateRootId();
82
        $this->messageRepository = new InMemoryMessageRepository();
83
        $dispatcher = $this->messageDispatcher();
84
        $decorator = $this->messageDecorator();
85
        $this->repository = $this->aggregateRootRepository(
86
            $className,
87
            $this->messageRepository,
88
            $dispatcher,
89
            $decorator
90
        );
91
        $this->expectedEvents = [];
92
        $this->assertedScenario = false;
93
        $this->theExpectedException = null;
94
        $this->caughtException = null;
95
    }
96
97
    protected function retrieveAggregateRoot(AggregateRootId $id): object
98
    {
99
        return $this->repository->retrieve($id);
100
    }
101
102
    protected function persistAggregateRoot(AggregateRoot $aggregateRoot): void
103
    {
104
        $this->repository->persist($aggregateRoot);
105
    }
106
107
    /**
108
     * @after
109
     */
110
    protected function assertScenario(): void
111
    {
112
        // @codeCoverageIgnoreStart
113
        if ($this->assertedScenario) {
114
            return;
115
        }
116
        // @codeCoverageIgnoreEnd
117
118
        try {
119
            $this->assertExpectedException($this->theExpectedException, $this->caughtException);
120
            $this->assertLastCommitEqualsEvents(...$this->expectedEvents);
121
            $this->messageRepository->purgeLastCommit();
122
        } finally {
123
            $this->assertedScenario = true;
124
            $this->theExpectedException = null;
125
            $this->caughtException = null;
126
        }
127
    }
128
129
    protected function aggregateRootId(): AggregateRootId
130
    {
131
        return $this->aggregateRootId;
132
    }
133
134
    abstract protected function newAggregateRootId(): AggregateRootId;
135
136
    abstract protected function aggregateRootClassName(): string;
137
138
    /**
139
     * @return $this
140
     */
141
    protected function given(object ...$events)
142
    {
143
        $this->repository->persistEvents($this->aggregateRootId(), count($events), ...$events);
144
        $this->messageRepository->purgeLastCommit();
145
146
        return $this;
147
    }
148
149
    /**
150
     * @return EventStager
151
     */
152
    public function on(AggregateRootId $id)
153
    {
154
        return new EventStager($id, $this->messageRepository, $this->repository, $this);
155
    }
156
157
    /**
158
     * @param mixed[] $arguments
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 pointInTime(): PointInTime
225
    {
226
        return $this->clock->pointInTime();
227
    }
228
229
    protected function clock(): Clock
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
    protected function aggregateRootRepository(
255
        string $className,
256
        MessageRepository $repository,
257
        MessageDispatcher $dispatcher,
258
        MessageDecorator $decorator
259
    ): AggregateRootRepository {
260
        return new ConstructingAggregateRootRepository(
261
            $className, $repository, $dispatcher, $decorator
262
        );
263
    }
264
}
265