Adding aggregate tests

Now that we have the basic components in place we can begin setting up our aggregate tests. These are the tests that we will use to verify the business logic for our application. Testing is one of the most valuable aspects of CQRS/event sourcing as it allows us to setup tests that have no coupling with our application logic.

This occurs because we rely only on events for past state, so no amount of refactoring of our application logic will affect the outcome. These tests should follow a pattern that you are likely familiar with:

  • Given some past events
  • When a command is applied
  • Then some result is expected

Let's first add a test module and define a new AccountTestFramework type for our test framework.

#[cfg(test)]
mod aggregate_tests {
    use super::*;
    use cqrs_es::test::TestFramework;

    type AccountTestFramework = TestFramework<BankAccount,BankAccountEvent>;
}

A first aggregate test

Now within our aggregate_tests module we will add our first test. Let's pass a DepositMoney command and we will expect to see a CustomerDepositedMoney event. Since we do not require any previous events, we can initiate our test with the given_no_previous_events method.

#[test]
fn test_deposit_money() {
    let expected = BankAccountEvent::CustomerDepositedMoney(CustomerDepositedMoney { amount: 200.0, balance: 200.0 });

    AccountTestFramework::default()
        .given_no_previous_events()
        .when(DepositMoney{ amount: 200.0 })
        .then_expect_events(vec![expected]);
}

Now if we run this test, we should see a test failure with the output looking something like this:

thread 'aggregate_tests::test' panicked at 'assertion failed: `(left == right)`
  left: `[]`,
 right: `[CustomerDepositedMoney(CustomerDepositedMoney { amount: 200.0, balance: 200.0 })]`', <::std::macros::panic ...

We have not added any logic yet, so this is what we should see. We have told the test to expect a CustomerDepositedMoney event, but none has been produced.

Adding business logic

Let's go back to our Command implementation for DepositMoney and fix this.

impl Command<BankAccount, BankAccountEvent> for DepositMoney {
    fn handle(self, account: &BankAccount) -> Result<Vec<BankAccountEvent>, AggregateError> {
        let balance = account.balance + self.amount;
        let event_payload = CustomerDepositedMoney {
            amount: self.amount,
            balance
        };
        Ok(vec![BankAccountEvent::CustomerDepositedMoney(event_payload)])
    }
}

And running our first test again - success!

Dealing with previous events

Now we should verify that our logic is valid if there is a previous balance. For this, we will use the given method to initiate the test, along with a vector containing a sole previous event:

#[test]
fn test_deposit_money_with_balance() {
    let previous = BankAccountEvent::CustomerDepositedMoney(CustomerDepositedMoney { amount: 200.0, balance: 200.0 });
    let expected = BankAccountEvent::CustomerDepositedMoney(CustomerDepositedMoney { amount: 200.0, balance: 400.0 });

    AccountTestFramework::default()
        .given(vec![previous])
        .when(DepositMoney{ amount: 200.0 })
        .then_expect_events(vec![expected]);
}

These exercises feel a little-brain dead, but provide a good example of how these tests are structured. Next we will start adding some real logic.