Article

Don't test aggregate state

April 29th, 2020

A shift in mindset when writing event-sourced aggregates.

Don't test aggregate state?

A few months ago I had an interesting conversation with Frank de Jonge, who has a pretty solid spot on my knowledgeable-people-list on a large number of topics. One of those topics happens to be Event Sourcing, we were discussing his Event Sourcing library EventSauce at the time.

As he was explaining to me how things fit together in this library, he explained how you can test an aggregate. It took us a little while to get through some miscommunication.

In event sourcing, there is a separation between firing events (saying something happened) and applying those events (actually changing state due to the thing that happened). Because when you want to take an aggregate out of persistence, you only want all the changes from the events to be applied, you don't want to say they happened again.

When we talked about "testing an aggregate", I was thinking about knowing that an event was applied correctly. But Frank was only showing how to confirm that an aggregate fired the correct events...

I though we where exploring the question:

How do you know if your aggregate correctly applies an event?

By using an example, the difference in our views emerged:

// This fires an event
$aggregate->doSomething();

// This changes the state of the aggregate
$aggregate->applySomethingWasDone(); 

How would that be tested? Logically (in my mind), we needed to get the aggregate in state A, then apply an event and finally test if the aggregate has state B. Pretty straightforward.

// Do something
$aggregate->property = 1
$aggregate->applyOneWasAdded()

// Then test the expected state
$aggregate->property == 2?

Because it's easier to discuss things with concrete examples, I took an example from a project called Elewant. This is a very small project, where you can keep track of the mascots of the PHP world, the plushy ElePHPants. In essence, you can start a Herd, and add/remove ElePHPants to/from your herd.

In Elewant, we used phpspec as a tool to write specifications, which are then run as tests. It's BDD, or Behavior Driven Development, so it's focus is on confirming how your model (code) behaves. Here is an example of the specification for adopting a new ElePHPant in your herd:

// Due to the way PHPspec works, $this represents a Herd, 
// and the $this->elePHPants() method returns it's members

public function it_adopts_one_new_elephpant(): void
{
    $this->adoptElePHPant(Breed::blueOriginalRegular());
    $this->elePHPants()->shouldHaveCount(1);
    $this->elePHPants()->shouldContainAnElePHPant(Breed::blueOriginalRegular());
}

Note that the specifications don't know (or care) that we are dealing with an event-sourced aggregate. The implementation might as well be replaced with something else and the domain would still function.

In that case: How do you know if your aggregate correctly generates an event?

Well, since our aggregate tests have no mention of events we don't even know if they exist at this point. So we need another test. In our specfications, the place where we are interested in the events is the place where we actually use them. Since we're using CQRS, this means the AdoptElePHPant command handler:

final class AdoptElePHPantHandler
{
    /**
     * @var HerdCollection
     */
    private $herdCollection;

    public function __construct(HerdCollection $herdCollection)
    {
        // The herdCollection is a repository that can load a Herd
        // When event sourcing, this means replaying all events for a herd with this herdId 
        $this->herdCollection = $herdCollection;
    }

    public function __invoke(AdoptElePHPant $command): void
    {
        $herd = $this->herdCollection->get($command->herdId());

        $herd->adoptElePHPant($command->breed());
        $this->herdCollection->save($herd);
    }
}

In our PHPspec specificatons, it looks like this:

// Due to the way PHPspec works, $this represents AdoptElePHPantHandler

public function it_handles_adopt_elephpant(): void
{
    // Arrange
    $herd = Herd::form(ShepherdId::fromString('00000000-0000-0000-0000-000000000000'), 'Herd name');
    $herdId = $herd->herdId();
    $this->herdCollection->get($herdId)->willReturn($herd);

    // Act
    $command = AdoptElePHPant::byHerd($herdId->toString(), Breed::WHITE_DPC_REGULAR);
    $this->__invoke($command);

    // Assert
    $events = $this->popRecordedEvent($herd);
    Assert::isInstanceOf($events[1], ElePHPantWasAdoptedByHerd::class);
    Assert::same($events[1]->payload()['breed'], Breed::WHITE_DPC_REGULAR);
}

This is probably the point where Frank shook his head and took me through his point of view.

Do not try to bend the spoon.

If we are dealing with an event sourced aggregate, we can say a few things about it:

  • It uses events to get to it's current state
  • It generates new events when something happens to it

Looking at it from that perspective, the behaviour is generating events. That's it. How about we write the following test:

$aggregate->doSomething();

$expectedEvent = new someThingWasDone();
$aggregate->popEvents() === $expectedEvent;

Note how we know nothing about the aggregate itself. We only know that the emitted events are correct. But... this can't be right? We can easily fool the tests into giving a false green light:

public function doSomething() {
    $this->recordThat(somethingWasDone);
}
public function applySomethingWasDone() {
    // Actually, do absolutely nothing. Mwahahaha!
    (to clarify: yes, I am making the Dr.Evil pinky move right now)
}

// In the test, this still holds true:
$expectedEvent = new someThingWasDone();
$aggregate->popEvents() === $expectedEvent

Then again... is there really a problem here? The state is not used directly anywhere outside the aggregate. It's like that age-old philosophical question:

When a tree falls in the forrest, and nobody is around to hear it, but the forrest is a projection based on tree events... how do you know if the developer set "$fallen = true" on the tree object?

The answer is, nobody cares. The rest of the system relies only on the events to represent the "truth". Every projection wil be correct, because the event has taken place (even though we did not actually change any state).

There is no spoon.

Let's go back to our herd's method to adopt a new elePHPant:

public function adoptElePHPant(Breed $breed): void
{
    $this->recordThat(
        ElePHPantWasAdoptedByHerd::tookPlace(
            $this->herdId,
            $breed
        )
    );
}

private function applyAnElePHPantWasAdoptedByHerd(ElePHPantWasAdoptedByHerd $event): void
{    
    $this->elePHPants[] = ElePHPant::appear($event->elePHPantId(), $event->breed());
}

In our new situation, we check that the event ElePHPantWasAdoptedByHerd is emitted after this call. If it is, the aggregate has done it's job properly. But what if we don't change the state of the Herd at all? What if the applyAnElePHPantWasAdoptedByHerd() { //does absolutely nothing }?

Maybe we should take a step back and think about what we use that state for anyways.

In my earlier example, there was an ->elePHPants() method on the herd that confirmed the addition of elePHPants after calling ->adoptElePHPant(). That looked like a proper confirmation of behavior, but in fact, the getter isn't actually used anywhere else in the application. It's only there for the tests.

And if we remove the getter, there is no reason to keep the local property at all. 😱

Follow the rules.

So when do we discover that our aggregate is as forgetful as Guy Pierce in Memento?

It happens after we we try to model the next behaviour. You see, we also want to allow you to remove ElePHPants from your herd (the domain term for that is abandonment, and it's frowned upon. Take good care of your plushies!).

We don't want the system to record impossible events, so the rule is:

You can't abandon an ElePHPant breed that you don't have.

public function abandonElePHPant(Breed $breed): void
{
    // This is where we throw an exception if you don't have the breed in question
    $this->guardContainsThisBreed($breed);

    $this->recordThat(
        ElePHPantWasAbandonedByHerd::tookPlace(
            $this->herdId,
            $breed
        )
    );
}

And the (pseudo-code) test would look like this:

It abandons a breed it does have:
Given: $herd->adoptElePHPant(ORIGINAL_BLUE);
When: $herd->abandonElePHPant(ORIGINAL_BLUE);

Then:
$expectedEvent = new ElePHPantWasAbandonedByHerd(ORIGINAL_BLUE);
$aggregate->popEvents() === $expectedEvent

Of course, we also need the test for when it should not happen:

It does not abandon a breed it does not have:
When: $herd->abandonElePHPant(ORIGINAL_BLUE);

Then:
Expect an exception (or at least: no event should be recorded)

How can we know if your herd contains a certain breed? Well, now we need to store some information during adoption. But instead of saving a "list of all elePHPants" like before, we could get away with less information. Like a list of all breeds you have, with a counter for each breed.

This is what it means to "protect invariants". The business rules must be obeyed, and in order to do so we must remember things about the aggregate, about the current state of it. But we don't need to have a perfect representation of everything there is to know though. If certain aspects of an aggregate have no particular rules, then we don't need to remember them. At least not until a new rule turns up that requires them.

A shift in mindset.

I wrote this blog post because even though I though I'd grasped the concept of Aggregates in an event sourced system, when it came to modelling the behavior I still automatically applied date-centric approach. And even though the system functions as intended, I'd been doing more work than required (by keeping state I did not need).

In a conversation about this topic with Jasper N. Brouwer, he mentioned Buttercup Protects by Mathias Verraes. We'd both read it quite some time ago - it explains the basics of event sourced aggregate design including protecting invariants.

But while I did learn about splitting the command ->doSomething() part from the event application applySomethingWasDone(), somehow the lesson regarding aggregate state slipped by. Or maybe it's just a habit or a blind spot to think in properties.

In any case, I found it interesting enough to share. Do you have any blind spots of your own? Let me know :-)

Ramon de la Fuente

Pointy haired boss

Do you have similar issues? Contact us at Future500

We can tackle your technical issues while you run your business.
Check out what we do or write us at info@future500.nl