src/Dudulina/Testing/BddAggregateTestHelper.php
<?php
/******************************************************************************
* Copyright (c) 2016 Constantin Galbenu <gica.galbenu@gmail.com> *
******************************************************************************/
namespace Dudulina\Testing;
use Dudulina\Command;
use Dudulina\Command\CommandApplier;
use Dudulina\Command\CommandSubscriber;
use Dudulina\Event;
use Dudulina\Event\EventDispatcher;
use Dudulina\Event\EventsApplier\EventsApplierOnAggregate;
use Dudulina\Event\EventWithMetaData;
use Dudulina\Event\MetaData;
use Dudulina\Testing\Exceptions\ExpectedEventNotYielded;
use Dudulina\Testing\Exceptions\NoExceptionThrown;
use Dudulina\Testing\Exceptions\TooManyEventsFired;
use Dudulina\Testing\Exceptions\WrongEventClassYielded;
use Dudulina\Testing\Exceptions\WrongExceptionClassThrown;
use Dudulina\Testing\Exceptions\WrongExceptionMessageWasThrown;
use Gica\CodeAnalysis\Shared\ClassComparison\SubclassComparator;
class BddAggregateTestHelper
{
private $aggregateId;
/** @var EventDispatcher */
private $eventDispatcher;
private $priorEvents = [];
/** @var Command */
private $command;
private $aggregate;
/** @var EventsApplierOnAggregate */
private $eventsApplierOnAggregate;
/** @var CommandApplier */
private $commandApplier;
/**
* @var CommandSubscriber
*/
private $commandSubscriber;
public function __construct(
CommandSubscriber $commandSubscriber
) {
$this->commandSubscriber = $commandSubscriber;
$this->eventsApplierOnAggregate = new EventsApplierOnAggregate();
$this->commandApplier = new CommandApplier();
$this->priorEvents = [];
$this->command = null;
}
private function getCommandSubscriber(): CommandSubscriber
{
return $this->commandSubscriber;
}
public function onAggregate($aggregate)
{
$this->aggregate = $aggregate;
$this->aggregateId = 123;
}
public function given(...$priorEvents)
{
$this->priorEvents = $this->decorateEventsWithMetadata($priorEvents);
}
/**
* @param Event[] $priorEvents
* @return EventWithMetaData[]
*/
private function decorateEventsWithMetadata(array $priorEvents)
{
return array_map(function ($event) {
return $this->decorateEventWithMetaData($event);
}, $priorEvents);
}
public function when(Command $command)
{
$this->command = $command;
}
public function then(...$expectedEvents)
{
$this->checkCommand($this->command);
$this->eventsApplierOnAggregate->applyEventsOnAggregate($this->aggregate, $this->priorEvents);
$newEvents = $this->executeCommand($this->command);
$this->assertTheseEvents($expectedEvents, $newEvents);
}
/**
* @param Command $command
* @return array
* @throws \Exception
*/
public function executeCommand(Command $command)
{
$handler = $this->getCommandSubscriber()->getHandlerForCommand($command);
$newEventsGenerator = $this->commandApplier->applyCommand($this->aggregate, $command, $handler->getMethodName());
/** @var EventWithMetaData[] $eventsWithMetaData */
$eventsWithMetaData = [];
$newEvents = [];
foreach ($newEventsGenerator as $event) {
$eventWithMetaData = $this->decorateEventWithMetaData($event);
$this->eventsApplierOnAggregate->applyEventsOnAggregate($this->aggregate, [$eventWithMetaData]);
$eventsWithMetaData[] = $eventWithMetaData;
$newEvents[] = $event;
}
return $newEvents;
}
private function decorateEventWithMetaData($event): EventWithMetaData
{
return new EventWithMetaData($event, $this->factoryMetaData());
}
public function thenShouldFailWith($exceptionClass, $message = null)
{
$this->checkCommand($this->command);
try {
$handler = $this->getCommandSubscriber()->getHandlerForCommand($this->command);
$this->eventsApplierOnAggregate->applyEventsOnAggregate($this->aggregate, $this->priorEvents);
iterator_to_array(
$this->commandApplier->applyCommand(
$this->aggregate, $this->command, $handler->getMethodName()));
throw new NoExceptionThrown(
sprintf("Exception '%s' was is expected, but none was thrown", $exceptionClass));
} catch (\Throwable $thrownException) {
if ($thrownException instanceof NoExceptionThrown) {
throw $thrownException;//rethrown
}
if (!$this->isClassOrSubClass($exceptionClass, $thrownException)) {
throw new WrongExceptionClassThrown(
sprintf(
"Exception '%s' was expected, but '%s(%s)' was thrown",
$exceptionClass,
get_class($thrownException),
$thrownException->getMessage()));
}
if ($message && $thrownException->getMessage() != $message) {
throw new WrongExceptionMessageWasThrown(
sprintf(
"Exception with message '%s' was expected, but '%s' was thrown",
$message,
$thrownException->getMessage()));
}
}
}
public function assertTheseEvents(array $expectedEvents, array $actualEvents)
{
$expectedEvents = array_values($expectedEvents);
$actualEvents = array_values($actualEvents);
$this->checkForToFewEvents($expectedEvents, $actualEvents);
$this->checkForToManyEvents(count($actualEvents) - count($expectedEvents));
}
private function checkForToFewEvents(array $expectedEvents, array $actualEvents)
{
foreach ($expectedEvents as $k => $expectedEvent) {
if (!isset($actualEvents[$k])) {
throw new ExpectedEventNotYielded(
"Expected event no. $k not fired (should have class: " . get_class($expectedEvent) . ")");
}
$actualEvent = $actualEvents[$k];
if ($this->hashEvent($expectedEvent) != $this->hashEvent($actualEvent)) {
throw new WrongEventClassYielded(
"Wrong event no. {$k} of class " . get_class($expectedEvent) . " emitted");
}
}
}
private function checkForToManyEvents(int $additionalCount)
{
if ($additionalCount > 0) {
throw new TooManyEventsFired(
sprintf("Additional %d events fired", $additionalCount));
}
}
public function hashEvent($event)
{
return array_merge(['___class' => get_class($event)], (array)($event));
}
private function factoryMetaData(): MetaData
{
return new MetaData(
$this->aggregateId, get_class($this->aggregate), new \DateTimeImmutable(), mt_rand()
);
}
private function isClassOrSubClass(string $parentClass, $childClass): bool
{
return (new SubclassComparator())->isASubClassOrSameClass($childClass, $parentClass);
}
private function checkCommand($command)
{
if (!$command instanceof Command) {
throw new \Exception("Command is missing. Have you called method when()?");
}
}
}