Passed
Pull Request — master (#84)
by Frank
02:43
created

AggregateRootTestCase::on()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
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 16
    protected function setUpEventSauce(): void
78
    {
79 16
        $className = $this->aggregateRootClassName();
80 16
        $this->clock = new TestClock();
81 16
        $this->aggregateRootId = $this->newAggregateRootId();
82 16
        $this->messageRepository = new InMemoryMessageRepository();
83 16
        $dispatcher = $this->messageDispatcher();
84 16
        $decorator = $this->messageDecorator();
85 16
        $this->repository = $this->aggregateRootRepository(
86 16
            $className,
87 16
            $this->messageRepository,
88 16
            $dispatcher,
89 16
            $decorator
90
        );
91 16
        $this->expectedEvents = [];
92 16
        $this->assertedScenario = false;
93 16
        $this->theExpectedException = null;
94 16
        $this->caughtException = null;
95 16
    }
96
97 4
    protected function retrieveAggregateRoot(AggregateRootId $id): object
98
    {
99 4
        return $this->repository->retrieve($id);
100
    }
101
102 4
    protected function persistAggregateRoot(AggregateRoot $aggregateRoot): void
103
    {
104 4
        $this->repository->persist($aggregateRoot);
105 4
    }
106
107
    /**
108
     * @after
109
     */
110 16
    protected function assertScenario(): void
111
    {
112
        // @codeCoverageIgnoreStart
113
        if ($this->assertedScenario) {
114
            return;
115
        }
116
        // @codeCoverageIgnoreEnd
117
118
        try {
119 16
            $this->assertExpectedException($this->theExpectedException, $this->caughtException);
120 13
            $this->assertLastCommitEqualsEvents(...$this->expectedEvents);
121 13
            $this->messageRepository->purgeLastCommit();
122 13
        } finally {
123 16
            $this->assertedScenario = true;
124 16
            $this->theExpectedException = null;
125 16
            $this->caughtException = null;
126
        }
127 13
    }
128
129 15
    protected function aggregateRootId(): AggregateRootId
130
    {
131 15
        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 5
    protected function given(object ...$events)
142
    {
143 5
        $this->repository->persistEvents($this->aggregateRootId(), count($events), ...$events);
144 5
        $this->messageRepository->purgeLastCommit();
145
146 5
        return $this;
147
    }
148
149 1
    public function on(AggregateRootId $id)
150
    {
151 1
        return new EventStager($id, $this->messageRepository, $this->repository, $this);
152
    }
153
154
    /**
155
     * @return $this
156
     */
157 14
    protected function when(...$arguments)
158
    {
159
        try {
160 14
            if ( ! method_exists($this, 'handle')) {
161 1
                throw new LogicException(sprintf('Class %s is missing a ::handle method.', get_class($this)));
162
            }
163
164 13
            $this->handle(...$arguments);
165 4
        } catch (Exception $exception) {
166 4
            $this->caughtException = $exception;
167
        }
168
169 14
        return $this;
170
    }
171
172
    /**
173
     * @return $this
174
     */
175 7
    protected function then(object ...$events)
176
    {
177 7
        $this->expectedEvents = $events;
178
179 7
        return $this;
180
    }
181
182
    /**
183
     * @return $this
184
     */
185 2
    public function expectToFail(Exception $expectedException)
186
    {
187 2
        $this->theExpectedException = $expectedException;
188
189 2
        return $this;
190
    }
191
192
    /**
193
     * @return $this
194
     */
195 3
    protected function thenNothingShouldHaveHappened()
196
    {
197 3
        $this->expectedEvents = [];
198
199 3
        return $this;
200
    }
201
202 13
    protected function assertLastCommitEqualsEvents(object ...$events): void
203
    {
204 13
        self::assertEquals($events, $this->messageRepository->lastCommit(), 'Events are not equal.');
205 13
    }
206
207 16
    private function assertExpectedException(
208
        Exception $expectedException = null,
209
        Exception $caughtException = null
210
    ): void {
211 16
        if (null !== $caughtException && (null === $expectedException || get_class($expectedException) !== get_class(
212 16
                    $caughtException
213
                ))) {
214 3
            throw $caughtException;
215
        }
216
217 13
        self::assertEquals([$expectedException], [$caughtException], '>> Exceptions are not equal.');
218 13
    }
219
220 1
    protected function pointInTime(): PointInTime
221
    {
222 1
        return $this->clock->pointInTime();
223
    }
224
225 9
    protected function clock(): Clock
226
    {
227 9
        return $this->clock;
228
    }
229
230 16
    protected function messageDispatcher(): MessageDispatcher
231
    {
232 16
        return new SynchronousMessageDispatcher(
233 16
            new MessageConsumerThatSerializesMessages(), ...$this->consumers()
234
        );
235
    }
236
237
    /**
238
     * @return MessageConsumer[]
239
     */
240 16
    protected function consumers(): array
241
    {
242 16
        return [];
243
    }
244
245 16
    private function messageDecorator(): MessageDecorator
246
    {
247 16
        return new MessageDecoratorChain(new DefaultHeadersDecorator());
248
    }
249
250 15
    protected function aggregateRootRepository(
251
        string $className,
252
        MessageRepository $repository,
253
        MessageDispatcher $dispatcher,
254
        MessageDecorator $decorator
255
    ): AggregateRootRepository {
256 15
        return new ConstructingAggregateRootRepository(
257 15
            $className, $repository, $dispatcher, $decorator
258
        );
259
    }
260
}
261