Testing Aggregates

Event sourced applications are very easy to test. EventSauce ships with test tooling that allows for scenario based testing using a given/when/then structure. This kind of testing is often associated with Behavior Driven Development (BDD).

Tests written in this style are very expressive and easy to read. This makes it simple to document business requirements. The test tooling also makes it easy to follow the TDD approach. Creating tests (and the required events) often gives very usable insights when modeling your application.

Create your own tooling!

Although EventSauce ships with its own set of test tooling, you’re encouraged to create your own. Every domain is different. Analyse your workflow and optimize common cases. Tackling a problem over and over? Try creating tooling for it!

1. Create a base test case for your aggregate.

It’s advised to create a base test for your aggregate. This base class sets up the defaults for all test cases around the aggregate. There are a couple of methods that need to be implemented:

  • newAggregateRootId is expected to return an aggregate root ID
  • aggregateRootClassName for the fully qualified aggregate root class name
  • handle executes the when input, usually by passing a command object to a method on the aggregate

You can choose to setup a handle method in your base test case or per test case. If you use a command-bus based interaction you’ll want to set it up in your base class.

use EventSauce\EventSourcing\AggregateRootId;
use EventSauce\EventSourcing\TestUtilities\AggregateRootTestCase;

abstract class SignUpProcessTestCase extends AggregateRootTestCase
{
    protected function newAggregateRootId(): AggregateRootId
    {
        return SignupId::create();
    }

    protected function aggregateRootClassName(): string
    {
        return SignUpProcess::class;
    }
    
    protected function handle(/* your arguments for when */)
    {
        // Implement me.
    }
}

In event sourcing it’s common to use commands as instructions for your system. You can use the aggregateRootId method to obtain a fixated ID (generated by your newAggregateRootId method). If done this way you can obtain the aggregate root like this:

use EventSauce\EventSourcing\TestUtilities\AggregateRootTestCase;

abstract class SignUpProcessTestCase extends AggregateRootTestCase
{
    // ...

    protected function handle(object $command)
    {
        if ($command instanceof SomeCommand) {
            $aggregate = $this->repository->retrieve($command->id());
            $aggregate->performAction();
            $this->repository->persist($aggregate);
        }
    }
}

2. Create test scenarios

Now that you’ve got your base class it’s time to set up test scenarios. It’s advised to create small, focused test cases that handle a specific task or condition.

For instance, initiating a sign-up process:

class InitiatingSignUpProcessTest extends SignUpProcessTestCase
{
    /**
     * @test
     */
    public function initiating_a_sign_up_process()
    {
        $processId = $this->aggregateRootId();

        $this->when(new InitiateSignUpProcess(
            $processId,
        ))->then(new SignUpProcessWasInitiated());
    } 
}

The when call takes the input needed to handle our scenario. The then clause specifies one or more events that we expect to be recorded afterwards.

In some cases you’ll want to record multiple events from a single interaction, this is also possible:

$this->when(new InitiateSignUpProcess(
    $processId
))->then(
    new SignUpProcessWasInitiated(),
    new AnotherThingHappened()
);

If we don’t expect something to happen, we can also express that. This allows you to test for idempotent handling of events:

class InitiatingSignUpProcessTest extends SignUpProcessTestCase
{
    /**
     * @test
     */
    public function not_recording_events()
    {
        $processId = $this->aggregateRootId();

        $this->when(new UneventfulCommand(
            $processId,
        ))->thenNothingShouldHaveHappened();
    }
}

The handle implementation for this command looks something like:

abstract class SignUpProcessTestCase extends AggregateRootTestCase
{
    protected function handle($command)
    {
        $process = $this->repository->retrieve($command->processId());
        
        if ($command instanceof InitiateSignUpProcess) {
            $process->initiate();           
        }
        
        $this->repository->persist($process);
    }
}
Frank de Jonge

EventSauce is a project by Frank de Jonge.