Passed
Pull Request — master (#84)
by Frank
09:52
created

AggregateRootTestCase::on()   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 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
     * @var AggregateRootRepository
40
     */
41
    protected $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
    abstract protected function aggregateRootClassName(): string;
136
137
    /**
138
     * @return $this
139
     */
140
    protected function given(object ...$events)
141
    {
142
        $this->repository->persistEvents($this->aggregateRootId(), count($events), ...$events);
143
        $this->messageRepository->purgeLastCommit();
144
145
        return $this;
146
    }
147
148
    /**
149
     * @return EventStager
150
     */
151
    public function on(AggregateRootId $id)
152
    {
153
        return new EventStager($id, $this->messageRepository, $this->repository, $this);
154
    }
155
156
    /**
157
     * @param mixed[] $arguments
158
     * @return $this
159
     */
160
    protected function when(...$arguments)
161
    {
162
        try {
163
            if ( ! method_exists($this, 'handle')) {
164
                throw new LogicException(sprintf('Class %s is missing a ::handle method.', get_class($this)));
165
            }
166
167
            $this->handle(...$arguments);
168
        } catch (Exception $exception) {
169
            $this->caughtException = $exception;
170
        }
171
172
        return $this;
173
    }
174
175
    /**
176
     * @return $this
177
     */
178
    protected function then(object ...$events)
179
    {
180
        $this->expectedEvents = $events;
181
182
        return $this;
183
    }
184
185
    /**
186
     * @return $this
187
     */
188
    public function expectToFail(Exception $expectedException)
189
    {
190
        $this->theExpectedException = $expectedException;
191
192
        return $this;
193
    }
194
195
    /**
196
     * @return $this
197
     */
198
    protected function thenNothingShouldHaveHappened()
199
    {
200
        $this->expectedEvents = [];
201
202
        return $this;
203
    }
204
205
    protected function assertLastCommitEqualsEvents(object ...$events): void
206
    {
207
        self::assertEquals($events, $this->messageRepository->lastCommit(), 'Events are not equal.');
208
    }
209
210
    private function assertExpectedException(
211
        Exception $expectedException = null,
212
        Exception $caughtException = null
213
    ): void {
214
        if (null !== $caughtException && (null === $expectedException || get_class($expectedException) !== get_class(
215
                    $caughtException
216
                ))) {
217
            throw $caughtException;
218
        }
219
220
        self::assertEquals([$expectedException], [$caughtException], '>> Exceptions are not equal.');
221
    }
222
223
    protected function currentTime(): DateTimeImmutable
224
    {
225
        return $this->clock->currentTime();
226
    }
227
228
    protected function clock(): TestClock
229
    {
230
        return $this->clock;
231
    }
232
233
    protected function messageDispatcher(): MessageDispatcher
234
    {
235
        return new SynchronousMessageDispatcher(
236
            new MessageConsumerThatSerializesMessages(), ...$this->consumers()
237
        );
238
    }
239
240
    /**
241
     * @return MessageConsumer[]
242
     */
243
    protected function consumers(): array
244
    {
245
        return [];
246
    }
247
248
    private function messageDecorator(): MessageDecorator
249
    {
250
        return new MessageDecoratorChain(new DefaultHeadersDecorator());
251
    }
252
253
    protected function aggregateRootRepository(
254
        string $className,
255
        MessageRepository $repository,
256
        MessageDispatcher $dispatcher,
257
        MessageDecorator $decorator
258
    ): AggregateRootRepository {
259
        return new ConstructingAggregateRootRepository(
260
            $className, $repository, $dispatcher, $decorator
261
        );
262
    }
263
}
264