diff --git a/src/lib.rs b/src/lib.rs index 3b6051e..a9503a8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -322,6 +322,8 @@ pub mod materialized_view; pub mod saga; /// Saga Manager module - belongs to the `Application` layer - composes pure saga and effects (publishing) pub mod saga_manager; +/// Given-When-Then Test specificatin domain specific language - unit testing +pub mod specification; /// View module - belongs to the `Domain` layer - pure event handling algorithm pub mod view; diff --git a/src/specification.rs b/src/specification.rs new file mode 100644 index 0000000..1ef923b --- /dev/null +++ b/src/specification.rs @@ -0,0 +1,201 @@ +//! ## A test specification DSL for deciders and views that supports the given-when-then format. + +use crate::{ + decider::{Decider, EventComputation, StateComputation}, + view::{View, ViewStateComputation}, +}; + +// ######################################################## +// ############# Decider Specification DSL ################ +// ######################################################## + +/// A test specification DSL for deciders that supports the `given-when-then` format. +/// The DSL is used to specify the events that have already occurred (GIVEN), the command that is being executed (WHEN), and the expected events (THEN) that should be generated. +pub struct DeciderTestSpecification<'a, Command, State, Event, Error> +where + Event: PartialEq + std::fmt::Debug, + Error: PartialEq + std::fmt::Debug, +{ + events: Vec, + state: Option, + command: Option, + decider: Option>, +} + +impl Default + for DeciderTestSpecification<'_, Command, State, Event, Error> +where + Event: PartialEq + std::fmt::Debug, + Error: PartialEq + std::fmt::Debug, +{ + fn default() -> Self { + Self { + events: Vec::new(), + state: None, + command: None, + decider: None, + } + } +} + +impl<'a, Command, State, Event, Error> DeciderTestSpecification<'a, Command, State, Event, Error> +where + Event: PartialEq + std::fmt::Debug, + State: PartialEq + std::fmt::Debug, + Error: PartialEq + std::fmt::Debug, +{ + #[allow(dead_code)] + /// Specify the decider you want to test + pub fn for_decider(mut self, decider: Decider<'a, Command, State, Event, Error>) -> Self { + self.decider = Some(decider); + self + } + + #[allow(dead_code)] + /// Given preconditions / previous events + pub fn given(mut self, events: Vec) -> Self { + self.events = events; + self + } + + #[allow(dead_code)] + /// Given preconditions / previous state + pub fn given_state(mut self, state: Option) -> Self { + self.state = state; + self + } + + #[allow(dead_code)] + /// When action/command + pub fn when(mut self, command: Command) -> Self { + self.command = Some(command); + self + } + + #[allow(dead_code)] + /// Then expect result / new events + pub fn then(self, expected_events: Vec) { + let decider = self + .decider + .expect("Decider must be initialized. Did you forget to call `for_decider`?"); + let command = self + .command + .expect("Command must be initialized. Did you forget to call `when`?"); + let events = self.events; + + let new_events_result = decider.compute_new_events(&events, &command); + let new_events = match new_events_result { + Ok(events) => events, + Err(error) => panic!( + "Events were expected but the decider returned an error instead: {:?}", + error + ), + }; + assert_eq!(new_events, expected_events); + } + + #[allow(dead_code)] + /// Then expect result / new events + pub fn then_state(self, expected_state: State) { + let decider = self + .decider + .expect("Decider must be initialized. Did you forget to call `for_decider`?"); + let command = self + .command + .expect("Command must be initialized. Did you forget to call `when`?"); + let state = self.state; + + let new_state_result = decider.compute_new_state(state, &command); + let new_state = match new_state_result { + Ok(state) => state, + Err(error) => panic!( + "State was expected but the decider returned an error instead: {:?}", + error + ), + }; + assert_eq!(new_state, expected_state); + } + + #[allow(dead_code)] + /// Then expect error result / these are not events + pub fn then_error(self, expected_error: Error) { + let decider = self + .decider + .expect("Decider must be initialized. Did you forget to call `for_decider`?"); + let command = self + .command + .expect("Command must be initialized. Did you forget to call `when`?"); + let events = self.events; + + let error_result = decider.compute_new_events(&events, &command); + let error = match error_result { + Ok(events) => panic!( + "An error was expected but the decider returned events instead: {:?}", + events + ), + Err(error) => error, + }; + assert_eq!(error, expected_error); + } +} + +// ######################################################## +// ############### View Specification DSL ################# +// ######################################################## + +/// A test specification DSL for views that supports the `given-then`` format. +/// The DSL is used to specify the events that have already occurred (GIVEN), and the expected view state (THEN) that should be generated based on these events. +pub struct ViewTestSpecification<'a, State, Event> +where + State: PartialEq + std::fmt::Debug, +{ + events: Vec, + view: Option>, +} + +impl Default for ViewTestSpecification<'_, State, Event> +where + State: PartialEq + std::fmt::Debug, +{ + fn default() -> Self { + Self { + events: Vec::new(), + view: None, + } + } +} + +impl<'a, State, Event> ViewTestSpecification<'a, State, Event> +where + State: PartialEq + std::fmt::Debug, +{ + #[allow(dead_code)] + /// Specify the view you want to test + pub fn for_view(mut self, view: View<'a, State, Event>) -> Self { + self.view = Some(view); + self + } + + #[allow(dead_code)] + /// Given preconditions / events + pub fn given(mut self, events: Vec) -> Self { + self.events = events; + self + } + + #[allow(dead_code)] + /// Then expect evolving new state of the view + pub fn then(self, expected_state: State) { + let view = self + .view + .expect("View must be initialized. Did you forget to call `for_view`?"); + + let events = self.events; + + let initial_state = (view.initial_state)(); + let event_refs: Vec<&Event> = events.iter().collect(); + let new_state_result = view.compute_new_state(Some(initial_state), &event_refs); + + assert_eq!(new_state_result, expected_state); + } +} diff --git a/tests/decider_test.rs b/tests/decider_test.rs index e870e72..d64ee30 100644 --- a/tests/decider_test.rs +++ b/tests/decider_test.rs @@ -1,11 +1,11 @@ -use fmodel_rust::decider::{Decider, EventComputation, StateComputation}; +use fmodel_rust::decider::Decider; +use fmodel_rust::specification::DeciderTestSpecification; use crate::api::{ - CancelOrderCommand, CreateOrderCommand, CreateShipmentCommand, OrderCancelledEvent, - OrderCommand, OrderCreatedEvent, OrderEvent, OrderState, OrderUpdatedEvent, ShipmentCommand, + CreateOrderCommand, CreateShipmentCommand, OrderCancelledEvent, OrderCommand, + OrderCreatedEvent, OrderEvent, OrderState, OrderUpdatedEvent, ShipmentCommand, ShipmentCreatedEvent, ShipmentEvent, ShipmentState, }; -use crate::application::Command::{OrderCreate, ShipmentCreate}; use crate::application::Event::{OrderCreated, ShipmentCreated}; use crate::application::{command_from_sum, event_from_sum, sum_to_event, Command, Event}; @@ -99,24 +99,46 @@ fn shipment_decider<'a>() -> Decider<'a, ShipmentCommand, ShipmentState, Shipmen } } -#[test] -fn test() { - let order_decider: Decider = order_decider(); - let order_decider_clone: Decider = crate::order_decider(); - let shipment_decider: Decider = - shipment_decider(); - let combined_decider: Decider = - order_decider_clone - .combine(shipment_decider) // Decider, (OrderState, ShipmentState), Sum> - .map_command(&command_from_sum) // Decider> - .map_event(&event_from_sum, &sum_to_event); // Decider +fn combined_decider<'a>() -> Decider<'a, Command, (OrderState, ShipmentState), Event> { + order_decider() + .combine(shipment_decider()) + .map_command(&command_from_sum) // Decider> + .map_event(&event_from_sum, &sum_to_event) +} +#[test] +fn create_order_event_sourced_test() { let create_order_command = CreateOrderCommand { order_id: 1, customer_name: "John Doe".to_string(), items: vec!["Item 1".to_string(), "Item 2".to_string()], }; + // Test the OrderDecider (event-sourced) + DeciderTestSpecification::default() + .for_decider(self::order_decider()) // Set the decider + .given(vec![]) // no existing events + .when(OrderCommand::Create(create_order_command.clone())) // Create an Order + .then(vec![OrderEvent::Created(OrderCreatedEvent { + order_id: 1, + customer_name: "John Doe".to_string(), + items: vec!["Item 1".to_string(), "Item 2".to_string()], + })]); + + // Test the Decider that combines OrderDecider and ShipmentDecider and can handle both OrderCommand and ShipmentCommand and produce Event (event-sourced) + DeciderTestSpecification::default() + .for_decider(self::combined_decider()) + .given(vec![]) + .when(Command::OrderCreate(create_order_command.clone())) + .then(vec![OrderCreated(OrderCreatedEvent { + order_id: 1, + customer_name: "John Doe".to_string(), + items: vec!["Item 1".to_string(), "Item 2".to_string()], + })]); +} + +#[test] +fn create_shipment_event_sourced_test() { let create_shipment_command = CreateShipmentCommand { shipment_id: 1, order_id: 1, @@ -124,59 +146,53 @@ fn test() { items: vec!["Item 1".to_string(), "Item 2".to_string()], }; - // Test the OrderDecider - let new_events = - order_decider.compute_new_events(&[], &OrderCommand::Create(create_order_command.clone())); - assert_eq!( - new_events, - Ok(vec![OrderEvent::Created(OrderCreatedEvent { - order_id: 1, - customer_name: "John Doe".to_string(), - items: vec!["Item 1".to_string(), "Item 2".to_string()], - })]) - ); - // Test the Decider that combines OrderDecider and ShipmentDecider and can handle both OrderCommand and ShipmentCommand and produce Event - let new_events2 = - combined_decider.compute_new_events(&[], &OrderCreate(create_order_command.clone())); - assert_eq!( - new_events2, - Ok(vec![OrderCreated(OrderCreatedEvent { - order_id: 1, - customer_name: "John Doe".to_string(), - items: vec!["Item 1".to_string(), "Item 2".to_string()], - })]) - ); - // Test the Decider that combines OrderDecider and ShipmentDecider and can handle both OrderCommand and ShipmentCommand and produce Event - let new_events3 = - combined_decider.compute_new_events(&[], &ShipmentCreate(create_shipment_command.clone())); - assert_eq!( - new_events3, - Ok(vec![ShipmentCreated(ShipmentCreatedEvent { + DeciderTestSpecification::default() + .for_decider(self::combined_decider()) + .given(vec![]) + .when(Command::ShipmentCreate(create_shipment_command.clone())) + .then(vec![ShipmentCreated(ShipmentCreatedEvent { shipment_id: 1, order_id: 1, customer_name: "John Doe".to_string(), items: vec!["Item 1".to_string(), "Item 2".to_string()], - })]) - ); + })]); +} + +#[test] +fn create_order_state_stored_test() { + let create_order_command = CreateOrderCommand { + order_id: 1, + customer_name: "John Doe".to_string(), + items: vec!["Item 1".to_string(), "Item 2".to_string()], + }; - // Test the OrderDecider - let new_state = - order_decider.compute_new_state(None, &OrderCommand::Create(create_order_command.clone())); - assert_eq!( - new_state, - Ok(OrderState { + // Test the OrderDecider (state stored) + DeciderTestSpecification::default() + .for_decider(self::order_decider()) // Set the decider + .given_state(None) // no existing state + .when(OrderCommand::Create(create_order_command.clone())) // Create an Order + .then_state(OrderState { order_id: 1, customer_name: "John Doe".to_string(), items: vec!["Item 1".to_string(), "Item 2".to_string()], is_cancelled: false, - }) - ); - // Test the Decider that combines OrderDecider and ShipmentDecider and can handle both OrderCommand and ShipmentCommand and produce a tuple of (OrderState, ShipmentState) - let new_state2 = - combined_decider.compute_new_state(None, &ShipmentCreate(create_shipment_command.clone())); - assert_eq!( - new_state2, - Ok(( + }); +} + +#[test] +fn create_shipment_state_stored_test() { + let create_shipment_command = CreateShipmentCommand { + shipment_id: 1, + order_id: 1, + customer_name: "John Doe".to_string(), + items: vec!["Item 1".to_string(), "Item 2".to_string()], + }; + // Test the Decider (state stored) that combines OrderDecider and ShipmentDecider and can handle both OrderCommand and ShipmentCommand and produce a tuple of (OrderState, ShipmentState) + DeciderTestSpecification::default() + .for_decider(self::combined_decider()) + .given_state(None) + .when(Command::ShipmentCreate(create_shipment_command.clone())) + .then_state(( OrderState { order_id: 0, customer_name: "".to_string(), @@ -188,28 +204,6 @@ fn test() { order_id: 1, customer_name: "John Doe".to_string(), items: vec!["Item 1".to_string(), "Item 2".to_string()], - } - )) - ); - - // Test the OrderDecider - let cancel_command = OrderCommand::Cancel(CancelOrderCommand { order_id: 1 }); - let new_events = order_decider.compute_new_events(&new_events.unwrap(), &cancel_command); - assert_eq!( - new_events, - Ok(vec![OrderEvent::Cancelled(OrderCancelledEvent { - order_id: 1 - })]) - ); - // Test the OrderDecider - let new_state = order_decider.compute_new_state(Some(new_state.unwrap()), &cancel_command); - assert_eq!( - new_state, - Ok(OrderState { - order_id: 1, - customer_name: "John Doe".to_string(), - items: vec!["Item 1".to_string(), "Item 2".to_string()], - is_cancelled: true, - }) - ); + }, + )); } diff --git a/tests/view_test.rs b/tests/view_test.rs index 30ef3e3..df4f884 100644 --- a/tests/view_test.rs +++ b/tests/view_test.rs @@ -1,9 +1,7 @@ -use fmodel_rust::view::{View, ViewStateComputation}; +use fmodel_rust::specification::ViewTestSpecification; +use fmodel_rust::view::View; -use crate::api::{ - OrderCancelledEvent, OrderCreatedEvent, OrderUpdatedEvent, OrderViewState, - ShipmentCreatedEvent, ShipmentViewState, -}; +use crate::api::{OrderCreatedEvent, OrderViewState, ShipmentCreatedEvent, ShipmentViewState}; use crate::application::Event; @@ -65,40 +63,32 @@ fn shipment_view<'a>() -> View<'a, ShipmentViewState, Event> { } } -#[test] -fn test() { - let order_view: View = order_view(); - let order_view2: View = crate::order_view(); - let shipment_view: View = shipment_view(); - - let merged_view = order_view2.merge(shipment_view); +fn merged_view<'a>() -> View<'a, (OrderViewState, ShipmentViewState), Event> { + order_view().merge(self::shipment_view()) +} +#[test] +fn order_created_view_test() { let order_created_event = Event::OrderCreated(OrderCreatedEvent { order_id: 1, customer_name: "John Doe".to_string(), items: vec!["Item 1".to_string(), "Item 2".to_string()], }); - let new_state = order_view.compute_new_state(None, &[&order_created_event]); - assert_eq!( - new_state, - OrderViewState { + ViewTestSpecification::default() + .for_view(self::order_view()) + .given(vec![order_created_event.clone()]) + .then(OrderViewState { order_id: 1, customer_name: "John Doe".to_string(), items: vec!["Item 1".to_string(), "Item 2".to_string()], is_cancelled: false, - } - ); - let order_created_event2 = Event::OrderCreated(OrderCreatedEvent { - order_id: 1, - customer_name: "John Doe".to_string(), - items: vec!["Item 1".to_string(), "Item 2".to_string()], - }); + }); - let new_merged_state2 = merged_view.compute_new_state(None, &[&order_created_event2]); - assert_eq!( - new_merged_state2, - ( + ViewTestSpecification::default() + .for_view(merged_view()) + .given(vec![order_created_event]) + .then(( OrderViewState { order_id: 1, customer_name: "John Doe".to_string(), @@ -110,20 +100,23 @@ fn test() { order_id: 0, customer_name: "".to_string(), items: Vec::new(), - } - ) - ); + }, + )); +} +#[test] - let shipment_created_event2 = Event::ShipmentCreated(ShipmentCreatedEvent { +fn shipment_created_view_test() { + let shipment_created_event = Event::ShipmentCreated(ShipmentCreatedEvent { shipment_id: 1, order_id: 1, customer_name: "John Doe".to_string(), items: vec!["Item 1".to_string(), "Item 2".to_string()], }); - let new_merged_state3 = merged_view.compute_new_state(None, &[&shipment_created_event2]); - assert_eq!( - new_merged_state3, - ( + + ViewTestSpecification::default() + .for_view(merged_view()) + .given(vec![shipment_created_event.clone()]) + .then(( OrderViewState { order_id: 0, customer_name: "".to_string(), @@ -135,46 +128,6 @@ fn test() { order_id: 1, customer_name: "John Doe".to_string(), items: vec!["Item 1".to_string(), "Item 2".to_string()], - } - ) - ); - - let order_updated_event = Event::OrderUpdated(OrderUpdatedEvent { - order_id: 1, - updated_items: vec![ - "Item 11".to_string(), - "Item 22".to_string(), - "Item 33".to_string(), - ], - }); - let new_state = order_view.compute_new_state(Some(new_state), &[&order_updated_event]); - assert_eq!( - new_state, - OrderViewState { - order_id: 1, - customer_name: "John Doe".to_string(), - items: vec![ - "Item 11".to_string(), - "Item 22".to_string(), - "Item 33".to_string() - ], - is_cancelled: false, - } - ); - - let order_canceled_event = Event::OrderCancelled(OrderCancelledEvent { order_id: 1 }); - let new_state = order_view.compute_new_state(Some(new_state), &[&order_canceled_event]); - assert_eq!( - new_state, - OrderViewState { - order_id: 1, - customer_name: "John Doe".to_string(), - items: vec![ - "Item 11".to_string(), - "Item 22".to_string(), - "Item 33".to_string() - ], - is_cancelled: true, - } - ); + }, + )); }