Oh Dear! monitors your entire website, not just the homepage. You'll get a notification as soon as your website is down, a monthly uptime report, a warning a few days before your SSL certificate expires and much more! Start your free 10 day trial now!

Implementing event sourcing: testing aggregates

Original – by Freek Van der Herten – 4 minute read

Earlier this year we released v2 of laravel-event-sourcing. This package is probably the easiest way to getting started with event sourcing in Laravel. A significant feature of v2 was the addition of aggregates.

Today we released another new version of the package that adds test methods. These methods allow you to verify if the aggregate behaves correctly. In this post, I'll show you an example and explain how the test methods are implemented.

These test methods were inspired by the awesome testing methods Frank De Jonge made in his Eventsauce package.

Testing an aggregate

An aggregate is a class that decides to record events based on past events. To know more about their general purpose and the idea behind them, read this section on using aggregates to make decisions-based-on-the-past. In the remainder of the post, we're going to assume that you know how to work with aggregates.

Imagine you have an AccountAggregateRoot that handles adding and subtract an amount for a bank account. The account has a limit of -$5000.

use Spatie\EventProjector\AggregateRoot;

class AccountAggregateRoot extends AggregateRoot
{
    /** @var int */
    private $balance = 0;

    /** @var int */
    private $accountLimit = -5000;

    public function createAccount(string $name, string $userId)
    {
        $this->recordThat(new AccountCreated($name, $userId));

        return $this;
    }

    public function addMoney(int $amount)
    {
        $this->recordThat(new MoneyAdded($amount));

        return $this;
    }

    protected function applyMoneyAdded(MoneyAdded $event)
    {
        $this->balance += $event->amount;
    }

    public function subtractMoney(int $amount)
    {
        $this->hasSufficientFundsToSubtractAmount($amount)
            ? $this->recordThat(new AccountLimitHit($amount))
            : $this->recordThat(new MoneySubtracted($amount));
    }

    protected function applyMoneySubtracted(MoneySubtracted $event)
    {
        $this->balance -= $event->amount;
    }

    private function hasSufficientFundsToSubtractAmount(int $amount): bool
    {
        return $this->balance - $amount >= $this->accountLimit;
    }
}

Let's now test that rule that an account cannot go beyond its limit.

// in a PHPUnit test

/** @test */
public function it_can_subtract_money()
{
    AccountAggregateRoot::fake()
        ->given(new SubtractMoney(4999))
        ->when(function (AccountAggregate $accountAggregate) {
            $accountAggregate->subtractMoney(1);
        })
        ->assertRecorded(new MoneySubtracted(1))
        ->assertNotRecorded(AccountLimitHit::class);
}

/** @test */
public function it_will_not_make_subtractions_that_would_go_below_the_account_limit()
{
    AccountAggregateRoot::fake()
        ->given(new SubtractMoney(4999))
        ->when(function (AccountAggregate $accountAggregate) {
            $accountAggregate->subtractMoney(2);
        })
        ->assertRecorded(new AccountLimitHit(2))
        ->assertNotRecorded(MoneySubtracted::class);
}

You could write the above test a bit shorter. The given events can be passed to the fake method. You're also not required to use the when function.

/** @test */
public function it_will_not_make_subtractions_that_would_go_below_the_account_limit()
{
    AccountAggregateRoot::fake(new SubtractMoney(4999))
        ->subtractMoney(2)
        ->assertRecorded(new AccountLimitHit(2))
        ->assertNotRecorded(MoneySubtracted::class);
}

Implementing aggregate test methods

Before starting implementing these test methods, I thought it was going to be a bit daunting. It turns out, it was not that hard.

To create an aggregate, you need to let a class extend our Spatie\EventProjector\AggregateRoot class.

class AccountAggregateRoot extends AggregateRoot {}

To keep the implementation of AggregateRoot clean, I wanted to avoid adding assertions on that class itself. The only method I added was fake. Calling this class will return a new FakeAggregateRoot instance in which the aggregate under test (in our example AccountAggregateRoot) is encapsulated.

Here is the implementation take from AggregateRoot.

/**
 * @param \Spatie\EventProjector\ShouldBeStored|\Spatie\EventProjector\ShouldBeStored[] $events
 *
 * @return $this
 */
public static function fake($events = []): FakeAggregateRoot
{
    $events = Arr::wrap($events);

    return (new FakeAggregateRoot(app(static::class)))->given($events);
}

In the FakeAggregateRoot all the test methods like given, when, assertRecorded and so on live.

Let's take a look at the simplest one: assertNothingRecorded.

public function assertNothingRecorded()
{
    PHPUnit\Framework\Assert::assertCount(0, $this->aggregateRoot->getRecordedEvents());
  
    return $this;
}

In the method above, we fetch the recorded events of the encapsulated aggregate root. A PHPUnit assertion is used to determine if that array is empty. If it is not, the PHPUnit test will fail.

If you're interested, you can take a look at the implementation of the other test methods.

In closing

I hope you've enjoyed this little tour of the new test methods. Previously I've written two more posts about the implementation details of the package: here's how the aggregates are implemented and here's a post on how the developer experience was improved.

To know more about the package in general read the introductionary post or the documentation.

Be sure to also check out this big list of PHP & Laravel packages our team has made previously.

Stay up to date with all things Laravel, PHP, and JavaScript.

Follow me on Twitter. I regularly tweet out programming tips, and what I myself have learned in ongoing projects.

Every two weeks I send out a newsletter containing lots of interesting stuff for the modern PHP developer.

Expect quick tips & tricks, interesting tutorials, opinions and packages. Because I work with Laravel every day there is an emphasis on that framework.

Rest assured that I will only use your email address to send you the newsletter and will not use it for any other purposes.

Comments

You can comment on this post by replying to this tweet.
Sergio G. Cruz Espinoza liked on 21st July 2019
Strotgen liked on 20th July 2019
Linka Softwares liked on 19th July 2019
Dries Vints retweeted on 19th July 2019
Erick Patrick liked on 19th July 2019
Sam Snelling liked on 19th July 2019
Mike liked on 19th July 2019