Home © Rinat Abdullin 🌟 AI Research · Newsletter · ML Labs · About

Specification Testing For Event Sourcing

When you no longer need to worry about persistence of A+ES entities, their captured behaviours tend to get more complex and intricate. In order to deliver software reliably in such conditions we need non-fragile and expressive way to capture and verify these behaviours with tests, while avoiding any regressions.

A+ES stands for Aggregates with Event Sourcing. This topic is covered in great detail in episodes of BeingTheWorst podcast.

In other words we need to ensure that:

  • tests will not break as we change internal structure of aggregates;
  • test should be expressive to capture easily any complex behavior;
  • they should match mental model of aggregate design and be understandable even by junior developers.

One solution is to focus on specific use cases using “specifications” or “given-when-then” tests. Within such tests we establish that:

  • given certain events;
  • when a command is executed (our case);
  • then we expect some specific events to happen.

Primary difference between specification and normal unit test is that the former explicitly define and describe a use case in a structured manner, while the latter just executes code. Each A+ES specification can be executed as a unit test, while the reverse is not necessarily true.

Due to strong synergy with DDD and no coupling to internal structural representation of A+ES entity, these tests capture intent and are not affected by internal refactorings (something common to CRUD-based Aggregate implementations)

In C# you can express such test case as:

[Test]
public void with_multiple_entries_and_previous_balance()
{
  Given(
    Price.SetPrice("salescast", 50m.Eur()),
    Price.SetPrice("forecast", 2m.Eur()),
    new CustomerCreated(id, "Landor", CurrencyType.Eur, guid, Date(2001)),
    new CustomerPaymentAdded(id, 1, 30m.Eur(), 30m.Eur(), "Prepaid", "magic", Date(2001)),
    ClockWasSet(2011, 3, 2)
  );

  When(
    new AddCustomerBill(id, bill, Date(2011, 2), Date(2011, 3), new[]
    {
      new CustomerBillEntry("salescast", 1),
      new CustomerBillEntry("forecast", 2),
      new CustomerBillEntry("forecast", 8)
    })
  );

  Expect(
    new CustomerBillChargeAdded(id, bill, Date(2011, 2), Date(2011, 3), new[]
    {
      new CustomerBillLine("salescast", "Test Product 'salescast'", 1, 50m.Eur()),
      new CustomerBillLine("forecast", "Test Product 'forecast'", 10, 20m.Eur()),
    }, 2, 70m.Eur(), -40m.Eur(), Date(2011, 3, 2))
  );
}

Test above is based on Lokad's version of A+ES Testing syntax, which was pushed to the master branch of Lokad.CQRS Sample Project. Look for spec_syntax class there.

Please note, that these specifications test A+ES entities at the level of application services (they accept command messages instead of method calls). This means that any Domain Services (helper classes that are passed by application service down to aggregate method call) are handled by the application service as well.

In this case we can use test implementations of domain services, configuring them via special events. Such events would be generated by helper methods (e.g.: Price.SetPrice("salescast", 50m.Eur()) or ClockWasSet(2011, 3, 2)). This allows us to reduce test fragility and also gain implicit documentation capabilities.

Specifications as Living Documentation

There are a few more side benefits of using specifications for testing business behaviours. First of all, specifications can act as a living documentation, which is always up-to-date. For instance, rendered documentation for the specification above would look like:

Test:          add customer bill
Specification: with multiple entries and previous balance

GIVEN:

  1. Set price of salescast to 50 EUR
  2. Set price of forecast to 2 EUR
  3. Created customer Customer-7 Eur 'Landor' with key 29c516fb-bdaf-48f5-a83d-d1dca263fdb6...
  4. Tx 1: payment 30 EUR 'Prepaid' (magic)
  5. Test clock set to 2011-03-02

WHEN:
  Add bill 1 from 2011-02-01 to 2011-03-01
    salescast : 1
    forecast  : 2
    forecast  : 8

THEN:

  1. Tx 2: charge for bill 1 from 2011-02-01 to 2011-03-01
       Test Product 'salescast'       (1 salescast): 50 EUR
       Test Product 'forecast'        (10 forecast): 20 EUR

Results: [Passed]

This can be achieved by merely overriding ToString() methods of event and command contract classes. Open source SimpleTesting sample can provide more details.

Detailed documentation of AR+ES behaviours that is defined in form of specifications, always stays up-to-date and in sync with the code changes.

Specifications as Design Tool

If we push this concept of living documentation further down the road, specifications can be used to communicate with business experts upon the use cases, using Ubiquituous Language and domain models. You can either express use cases in text as “Given-When-Then”, have junior developer code them as unit tests and then ask domain experts to implement functionality.

Additional practical usage scenarios for specifications include:

  • You can print out all specifications as a really thorough list of use-cases for signing off by project stakeholders.
  • Specifications can easily be visualized as diagrams and graphs. They could help in better understanding of your domain, finding non-tested or complicated spots and driving development in general.

For instance, such diagram could look like:

Hope, this helps. I plan to cover this topic in greater detail in upcoming episodes of BeingTheWorst podcast.

Published: September 18, 2012.

🤗 Check out my newsletter! It is about building products with ChatGPT and LLMs: latest news, technical insights and my journey. Check out it out