eventually/aggregate/
test.rs

1//! Module exposing a [Scenario] type to test [Aggregate]s using
2//! the [given-then-when canvas](https://www.agilealliance.org/glossary/gwt/).
3
4use std::fmt::Debug;
5use std::marker::PhantomData;
6use std::ops::Deref;
7use std::sync::Arc;
8
9use crate::aggregate::{Aggregate, Root};
10use crate::event;
11
12/// A test scenario that can be used to test an [Aggregate] and [Aggregate Root][Root]
13/// using a [given-then-when canvas](https://www.agilealliance.org/glossary/gwt/) approach.
14#[derive(Clone, Copy)]
15pub struct Scenario<T>(PhantomData<T>)
16where
17    T: Aggregate,
18    T::Id: Clone,
19    T::Event: Debug + PartialEq,
20    T::Error: Debug;
21
22impl<T> Scenario<T>
23where
24    T: Aggregate,
25    T::Id: Clone,
26    T::Event: Debug + PartialEq,
27    T::Error: Debug,
28{
29    /// Creates a new [Scenario] instance.
30    #[must_use]
31    pub fn new() -> Self {
32        Self(PhantomData)
33    }
34
35    /// Specifies the precondition for the test [Scenario].
36    ///
37    /// In other words, it can be used to specify all the Domain [Event][event::Envelope]s
38    /// that make up the state of the [Aggregate Root][Root].
39    #[must_use]
40    pub fn given(self, events: Vec<event::Envelope<T::Event>>) -> ScenarioGiven<T> {
41        ScenarioGiven {
42            events,
43            marker: PhantomData,
44        }
45    }
46
47    /// Specifies the action/mutation to execute in this [Scenario].
48    ///
49    /// Use this branch when testing actions/mutations that create new [Aggregate Root][Root]
50    /// instances, i.e. with no prior Domain Events recorded.
51    #[must_use]
52    pub fn when<R, F, Err>(self, f: F) -> ScenarioWhen<T, R, F, Err>
53    where
54        R: From<Root<T>>,
55        F: Fn() -> Result<R, Err>,
56    {
57        ScenarioWhen {
58            mutate: f,
59            marker: PhantomData,
60            err_marker: PhantomData,
61            root_marker: PhantomData,
62        }
63    }
64}
65
66impl<T> Default for Scenario<T>
67where
68    T: Aggregate,
69    T::Id: Clone,
70    T::Event: Debug + PartialEq,
71    T::Error: Debug,
72{
73    fn default() -> Self {
74        Self::new()
75    }
76}
77
78#[doc(hidden)]
79pub struct ScenarioGiven<T>
80where
81    T: Aggregate,
82    T::Id: Clone,
83    T::Event: Debug + PartialEq,
84    T::Error: Debug,
85{
86    events: Vec<event::Envelope<T::Event>>,
87    marker: PhantomData<T>,
88}
89
90impl<T> ScenarioGiven<T>
91where
92    T: Aggregate,
93    T::Id: Clone,
94    T::Event: Debug + PartialEq,
95    T::Error: Debug,
96{
97    /// Specifies the action/mutation to execute in this [Scenario].
98    ///
99    /// Use this branch when testing actions/mutations that modify the state
100    /// of an [Aggregate Root][Root] that already exists, by specifying its
101    /// current state using [`Scenario::given`].
102    ///
103    /// # Panics
104    ///
105    /// Please note: as this method expects that an [Aggregate Root][Root] instance
106    /// is available when executing the domain method, it will panic if a `Root` instance
107    /// could not be obtained by rehydrating the [`Aggregate`] state through the events
108    /// provided in [`Scenario::given`].
109    #[must_use]
110    pub fn when<R, F, Err>(self, f: F) -> ScenarioWhen<T, R, impl Fn() -> Result<R, Err>, Err>
111    where
112        R: From<Root<T>>,
113        F: Fn(&mut R) -> Result<(), Err>,
114    {
115        let events = Arc::new(self.events);
116
117        ScenarioWhen {
118            marker: PhantomData,
119            err_marker: PhantomData,
120            root_marker: PhantomData,
121            mutate: move || -> Result<R, Err> {
122                let mut root: R = Root::<T>::rehydrate(events.iter().cloned())
123                    .expect(
124                        "no error is expected when applying domain events from a 'given' clause",
125                    )
126                    .expect("an aggregate root instance is expected, but none was produced")
127                    .into();
128
129                match f(&mut root) {
130                    Ok(()) => Ok(root),
131                    Err(err) => Err(err),
132                }
133            },
134        }
135    }
136}
137
138#[doc(hidden)]
139pub struct ScenarioWhen<T, R, F, Err>
140where
141    T: Aggregate,
142    T::Event: Debug + PartialEq,
143    R: From<Root<T>>,
144    F: Fn() -> Result<R, Err>,
145{
146    mutate: F,
147    marker: PhantomData<T>,
148    err_marker: PhantomData<Err>,
149    root_marker: PhantomData<R>,
150}
151
152impl<T, R, F, Err> ScenarioWhen<T, R, F, Err>
153where
154    T: Aggregate,
155    T::Event: Debug + PartialEq,
156    R: From<Root<T>> + Deref<Target = Root<T>>,
157    F: Fn() -> Result<R, Err>,
158{
159    /// Specifies that the outcome of the [Scenario] is positive, and
160    /// should result in the creation of the specified Domain Events.
161    #[must_use]
162    pub fn then(self, result: Vec<event::Envelope<T::Event>>) -> ScenarioThen<T, R, F, Err> {
163        ScenarioThen {
164            mutate: self.mutate,
165            expected: Ok(result),
166            marker: PhantomData,
167        }
168    }
169
170    /// Specified that the outcome of the [Scenario] is negative.
171    ///
172    /// Use this method to assert the specific Error value that the
173    /// [Aggregate Root][Root] method should return.
174    #[must_use]
175    pub fn then_error(self, err: Err) -> ScenarioThen<T, R, F, Err> {
176        ScenarioThen {
177            mutate: self.mutate,
178            expected: Err(err),
179            marker: PhantomData,
180        }
181    }
182}
183
184#[doc(hidden)]
185pub struct ScenarioThen<T, R, F, Err>
186where
187    T: Aggregate,
188    T::Event: Debug + PartialEq,
189    R: From<Root<T>> + Deref<Target = Root<T>>,
190    F: Fn() -> Result<R, Err>,
191{
192    mutate: F,
193    expected: Result<Vec<event::Envelope<T::Event>>, Err>,
194    marker: PhantomData<R>,
195}
196
197impl<T, R, F, Err> ScenarioThen<T, R, F, Err>
198where
199    T: Aggregate,
200    T::Event: Debug + PartialEq,
201    R: From<Root<T>> + Deref<Target = Root<T>>,
202    F: Fn() -> Result<R, Err>,
203    Err: PartialEq + Debug,
204{
205    /// Runs the [Scenario] and performs the various assertion for the test.
206    ///
207    /// # Panics
208    ///
209    /// This method will panic if the assertions have not passed, making
210    /// the test fail.
211    pub fn assert(self) {
212        let result = (self.mutate)().map(|root| root.recorded_events.clone());
213        assert_eq!(self.expected, result);
214    }
215}