use crate::aggregate::AggregateRoot;
use std::fmt::Debug;
use std::marker::PhantomData;
pub struct TestFramework<A: AggregateRoot> {
aggregate: A,
_phantom: PhantomData<A>,
}
impl<A: AggregateRoot> TestFramework<A> {
pub fn with(aggregate: A) -> Self {
Self {
aggregate,
_phantom: PhantomData,
}
}
}
impl<A: AggregateRoot> TestFramework<A> {
pub fn given_no_previous_events(self) -> WhenPhase<A> {
WhenPhase {
aggregate: self.aggregate,
initial_events: Vec::new(),
}
}
pub fn given(mut self, events: Vec<A::DomainEvent>) -> WhenPhase<A> {
for event in &events {
self.aggregate.apply(event.clone());
}
WhenPhase {
aggregate: self.aggregate,
initial_events: events,
}
}
pub fn given_event(self, event: A::DomainEvent) -> WhenPhase<A> {
self.given(vec![event])
}
}
pub struct WhenPhase<A: AggregateRoot> {
aggregate: A,
initial_events: Vec<A::DomainEvent>,
}
impl<A: AggregateRoot> WhenPhase<A> {
pub fn when(mut self, command: A::Command) -> ThenPhase<A> {
let result = self.aggregate.handle(command);
ThenPhase {
aggregate: self.aggregate,
initial_events: self.initial_events,
result,
}
}
}
pub struct ThenPhase<A: AggregateRoot> {
aggregate: A,
#[allow(dead_code)]
initial_events: Vec<A::DomainEvent>,
result: Result<Vec<A::DomainEvent>, A::Error>,
}
impl<A: AggregateRoot> ThenPhase<A>
where
A::DomainEvent: Debug + PartialEq,
A::Error: Debug,
{
pub fn then_expect_events(self, expected_events: Vec<A::DomainEvent>) {
match self.result {
Ok(actual_events) => {
assert_eq!(
actual_events, expected_events,
"Expected events do not match actual events.\nExpected: {expected_events:?}\nActual: {actual_events:?}"
);
}
Err(e) => {
panic!("Expected events but got error: {e:?}");
}
}
}
pub fn then_expect_event(self, expected_event: A::DomainEvent) {
self.then_expect_events(vec![expected_event])
}
pub fn then_expect_no_events(self) {
self.then_expect_events(vec![])
}
pub fn then_expect_error<E>(self) -> E
where
E: Debug,
A::Error: Into<E>,
{
match self.result {
Ok(events) => {
panic!("Expected error but got events: {events:?}");
}
Err(error) => error.into(),
}
}
pub fn then_expect_error_matches<F>(self, predicate: F)
where
F: FnOnce(&A::Error) -> bool,
{
match self.result {
Ok(events) => {
panic!("Expected error but got events: {events:?}");
}
Err(ref error) => {
assert!(predicate(error), "Error does not match expected predicate: {error:?}");
}
}
}
pub fn then_aggregate_state<F>(mut self, assertion: F)
where
F: FnOnce(&A),
{
if let Ok(events) = &self.result {
for event in events {
self.aggregate.apply(event.clone());
}
}
assertion(&self.aggregate);
}
pub fn then_verify<F>(self, verification: F)
where
F: FnOnce(Result<Vec<A::DomainEvent>, A::Error>),
{
verification(self.result);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
aggregate_id::{AggregateId, HasIdPrefix},
command::Command,
domain_event::DomainEvent,
event_id::EventIdType,
integration_event::IntegrationEvent,
message::Message,
AggregateRoot,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
struct TestId;
impl HasIdPrefix for TestId {
const PREFIX: &'static str = "test";
}
#[derive(Debug, Clone, PartialEq)]
struct TestAggregate {
id: AggregateId<TestId>,
value: i32,
is_active: bool,
}
#[derive(Debug, Clone, PartialEq)]
enum TestCommand {
Create { id: AggregateId<TestId> },
UpdateValue { value: i32 },
Deactivate,
}
impl Message for TestCommand {
fn name(&self) -> &'static str {
match self {
TestCommand::Create { .. } => "Create",
TestCommand::UpdateValue { .. } => "UpdateValue",
TestCommand::Deactivate => "Deactivate",
}
}
}
impl Command for TestCommand {
type ID = TestId;
fn id(&self) -> AggregateId<Self::ID> {
match self {
TestCommand::Create { id } => *id,
TestCommand::UpdateValue { .. } => panic!("UpdateValue command requires aggregate to exist"),
TestCommand::Deactivate => panic!("Deactivate command requires aggregate to exist"),
}
}
}
#[derive(Debug, Clone, PartialEq)]
enum TestEvent {
Created { id: AggregateId<TestId> },
ValueUpdated { value: i32 },
Deactivated,
}
impl Message for TestEvent {
fn name(&self) -> &'static str {
match self {
TestEvent::Created { .. } => "Created",
TestEvent::ValueUpdated { .. } => "ValueUpdated",
TestEvent::Deactivated => "Deactivated",
}
}
}
impl DomainEvent for TestEvent {
fn id(&self) -> EventIdType {
EventIdType::new()
}
fn event_type(&self) -> &'static str {
self.name()
}
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
struct TestIntegrationEvent {
#[allow(dead_code)]
message: String,
}
impl Message for TestIntegrationEvent {
fn name(&self) -> &'static str {
"TestIntegrationEvent"
}
}
impl IntegrationEvent for TestIntegrationEvent {
fn id(&self) -> String {
ulid::Ulid::new().to_string()
}
fn event_type(&self) -> &'static str {
"TestIntegrationEvent"
}
}
#[derive(Debug, thiserror::Error)]
enum TestError {
#[error("Already created")]
AlreadyCreated,
#[error("Not active")]
NotActive,
#[error("Invalid value")]
InvalidValue,
}
impl AggregateRoot for TestAggregate {
const TYPE: &'static str = "TestAggregate";
type ID = TestId;
type Command = TestCommand;
type DomainEvent = TestEvent;
type Error = TestError;
fn init(id: AggregateId<Self::ID>) -> Self {
Self {
id,
value: 0,
is_active: false,
}
}
fn id(&self) -> &AggregateId<Self::ID> {
&self.id
}
fn handle(&mut self, command: Self::Command) -> Result<Vec<Self::DomainEvent>, Self::Error> {
match command {
TestCommand::Create { id } => {
if self.is_active {
return Err(TestError::AlreadyCreated);
}
Ok(vec![TestEvent::Created { id }])
}
TestCommand::UpdateValue { value } => {
if !self.is_active {
return Err(TestError::NotActive);
}
if value < 0 {
return Err(TestError::InvalidValue);
}
Ok(vec![TestEvent::ValueUpdated { value }])
}
TestCommand::Deactivate => {
if !self.is_active {
return Err(TestError::NotActive);
}
Ok(vec![TestEvent::Deactivated])
}
}
}
fn apply(&mut self, event: Self::DomainEvent) {
match event {
TestEvent::Created { id } => {
self.id = id;
self.is_active = true;
}
TestEvent::ValueUpdated { value } => {
self.value = value;
}
TestEvent::Deactivated => {
self.is_active = false;
}
}
}
}
#[test]
fn test_given_no_previous_events() {
let id = AggregateId::<TestId>::new();
let aggregate = TestAggregate::init(id);
TestFramework::with(aggregate)
.given_no_previous_events()
.when(TestCommand::Create { id })
.then_expect_event(TestEvent::Created { id });
}
#[test]
fn test_given_with_events() {
let id = AggregateId::<TestId>::new();
let aggregate = TestAggregate::init(id);
TestFramework::with(aggregate)
.given(vec![TestEvent::Created { id }])
.when(TestCommand::UpdateValue { value: 42 })
.then_expect_event(TestEvent::ValueUpdated { value: 42 });
}
#[test]
fn test_expect_error() {
let id = AggregateId::<TestId>::new();
let aggregate = TestAggregate::init(id);
TestFramework::with(aggregate)
.given_no_previous_events()
.when(TestCommand::UpdateValue { value: 10 })
.then_expect_error::<TestError>();
}
#[test]
fn test_expect_error_matches() {
let id = AggregateId::<TestId>::new();
let aggregate = TestAggregate::init(id);
TestFramework::with(aggregate)
.given_no_previous_events()
.when(TestCommand::UpdateValue { value: 10 })
.then_expect_error_matches(|e| matches!(e, TestError::NotActive));
}
#[test]
fn test_aggregate_state_assertion() {
let id = AggregateId::<TestId>::new();
let aggregate = TestAggregate::init(id);
TestFramework::with(aggregate)
.given(vec![TestEvent::Created { id }])
.when(TestCommand::UpdateValue { value: 99 })
.then_aggregate_state(|agg| {
assert_eq!(*agg.id(), id);
assert_eq!(agg.value, 99);
assert!(agg.is_active);
});
}
#[test]
fn test_deactivate_already_inactive() {
let id = AggregateId::<TestId>::new();
let aggregate = TestAggregate::init(id);
TestFramework::with(aggregate)
.given(vec![TestEvent::Created { id }, TestEvent::Deactivated])
.when(TestCommand::Deactivate)
.then_expect_error_matches(|e| matches!(e, TestError::NotActive));
}
}