eventually/command/
test.rs

1//! Module exposing a test [Scenario] type to write Domain [Command][command::Envelope]s
2//! test cases using the [given-then-when canvas](https://www.agilealliance.org/glossary/gwt/).
3
4use std::fmt::Debug;
5use std::hash::Hash;
6
7use crate::event::store::{Appender, EventStoreExt};
8use crate::{command, event, message, version};
9
10/// A test scenario that can be used to test a [Command][command::Envelope] [Handler][command::Handler]
11/// using a [given-then-when canvas](https://www.agilealliance.org/glossary/gwt/) approach.
12pub struct Scenario;
13
14impl Scenario {
15    /// Sets the precondition state of the system for the [Scenario], which
16    /// is expressed by a list of Domain [Event][event::Envelope]s in an Event-sourced system.
17    #[must_use]
18    pub fn given<Id, Evt>(self, events: Vec<event::Persisted<Id, Evt>>) -> ScenarioGiven<Id, Evt>
19    where
20        Evt: message::Message,
21    {
22        ScenarioGiven { given: events }
23    }
24
25    /// Specifies the [Command][command::Envelope] to test in the [Scenario], in the peculiar case
26    /// of having a clean system.
27    ///
28    /// This is a shortcut for:
29    /// ```text
30    /// Scenario::given(vec![]).when(...)
31    /// ```
32    #[must_use]
33    pub fn when<Id, Evt, Cmd>(self, command: command::Envelope<Cmd>) -> ScenarioWhen<Id, Evt, Cmd>
34    where
35        Evt: message::Message,
36        Cmd: message::Message,
37    {
38        ScenarioWhen {
39            given: Vec::default(),
40            when: command,
41        }
42    }
43}
44
45#[doc(hidden)]
46pub struct ScenarioGiven<Id, Evt>
47where
48    Evt: message::Message,
49{
50    given: Vec<event::Persisted<Id, Evt>>,
51}
52
53impl<Id, Evt> ScenarioGiven<Id, Evt>
54where
55    Evt: message::Message,
56{
57    /// Specifies the [Command][command::Envelope] to test in the [Scenario].
58    #[must_use]
59    pub fn when<Cmd>(self, command: command::Envelope<Cmd>) -> ScenarioWhen<Id, Evt, Cmd>
60    where
61        Cmd: message::Message,
62    {
63        ScenarioWhen {
64            given: self.given,
65            when: command,
66        }
67    }
68}
69
70#[doc(hidden)]
71pub struct ScenarioWhen<Id, Evt, Cmd>
72where
73    Evt: message::Message,
74    Cmd: message::Message,
75{
76    given: Vec<event::Persisted<Id, Evt>>,
77    when: command::Envelope<Cmd>,
78}
79
80impl<Id, Evt, Cmd> ScenarioWhen<Id, Evt, Cmd>
81where
82    Evt: message::Message,
83    Cmd: message::Message,
84{
85    /// Sets the expectation on the result of the [Scenario] to be positive
86    /// and produce a specified list of Domain [Event]s.
87    #[must_use]
88    pub fn then(self, events: Vec<event::Persisted<Id, Evt>>) -> ScenarioThen<Id, Evt, Cmd> {
89        ScenarioThen {
90            given: self.given,
91            when: self.when,
92            case: ScenarioThenCase::Produces(events),
93        }
94    }
95
96    /// Sets the expectation on the result of the [Scenario] to return an error.
97    #[must_use]
98    pub fn then_fails(self) -> ScenarioThen<Id, Evt, Cmd> {
99        ScenarioThen {
100            given: self.given,
101            when: self.when,
102            case: ScenarioThenCase::Fails,
103        }
104    }
105}
106
107enum ScenarioThenCase<Id, Evt>
108where
109    Evt: message::Message,
110{
111    Produces(Vec<event::Persisted<Id, Evt>>),
112    Fails,
113}
114
115#[doc(hidden)]
116pub struct ScenarioThen<Id, Evt, Cmd>
117where
118    Evt: message::Message,
119    Cmd: message::Message,
120{
121    given: Vec<event::Persisted<Id, Evt>>,
122    when: command::Envelope<Cmd>,
123    case: ScenarioThenCase<Id, Evt>,
124}
125
126impl<Id, Evt, Cmd> ScenarioThen<Id, Evt, Cmd>
127where
128    Id: Clone + Eq + Hash + Send + Sync + Debug,
129    Evt: message::Message + Clone + PartialEq + Send + Sync + Debug,
130    Cmd: message::Message,
131{
132    /// Executes the whole [Scenario] by constructing a Command [Handler][command::Handler]
133    /// with the provided closure function and running the specified assertions.
134    ///
135    /// # Panics
136    ///
137    /// The method panics if the assertion fails.
138    pub async fn assert_on<F, H>(self, handler_factory: F)
139    where
140        F: Fn(event::store::Tracking<event::store::InMemory<Id, Evt>, Id, Evt>) -> H,
141        H: command::Handler<Cmd>,
142    {
143        let event_store = event::store::InMemory::<Id, Evt>::default();
144        let tracking_event_store = event_store.clone().with_recorded_events_tracking();
145
146        for event in self.given {
147            event_store
148                .append(
149                    event.stream_id,
150                    version::Check::MustBe(event.version - 1),
151                    vec![event.event],
152                )
153                .await
154                .expect("domain event in 'given' should be inserted in the event store");
155        }
156
157        let handler = handler_factory(tracking_event_store.clone());
158        let result = handler.handle(self.when).await;
159
160        match self.case {
161            ScenarioThenCase::Produces(events) => {
162                let recorded_events = tracking_event_store.recorded_events();
163                assert_eq!(events, recorded_events);
164            },
165            ScenarioThenCase::Fails => assert!(result.is_err()),
166        }
167    }
168}