thalo_testing/
lib.rs

1//! Testing utilities for [thalo](https://docs.rs/thalo) apps.
2//!
3//! # Examples
4//!
5//! Create aggregate and events.
6//!
7//! ```
8//! use thalo::{
9//!     aggregate::{Aggregate, TypeId},
10//!     event::EventType,
11//! };
12//! use thiserror::Error;
13//!
14//! #[derive(Aggregate, Clone, Debug, Default, PartialEq, TypeId)]
15//! struct BankAccount {
16//!     id: String,
17//!     opened: bool,
18//!     balance: f64,
19//! }
20//!
21//! #[derive(Clone, Debug, EventType)]
22//! enum BankAccountEvent {
23//!     OpenedAccount { balance: f64 },
24//! }
25//!
26//! fn apply(bank_account: &mut BankAccount, event: BankAccountEvent) {
27//!     use BankAccountEvent::*;
28//!
29//!     match event {
30//!         OpenedAccount { balance } => {
31//!             bank_account.opened = true;
32//!             bank_account.balance = balance;
33//!         }
34//!     }
35//! }
36//! ```
37//!
38//! Test aggregate events.
39//!
40//! ```
41//! # use thalo::{
42//! #     aggregate::{Aggregate, TypeId},
43//! #     event::EventType,
44//! # };
45//! # use thiserror::Error;
46//! #
47//! # #[derive(Aggregate, Clone, Debug, Default, PartialEq, TypeId)]
48//! # struct BankAccount {
49//! #     id: String,
50//! #     opened: bool,
51//! #     balance: f64,
52//! # }
53//! #
54//! # #[derive(Clone, Debug, EventType)]
55//! # enum BankAccountEvent {
56//! #     OpenedAccount { balance: f64 },
57//! # }
58//! #
59//! # fn apply(bank_account: &mut BankAccount, event: BankAccountEvent) {
60//! #     use BankAccountEvent::*;
61//! #
62//! #     match event {
63//! #         OpenedAccount { balance } => {
64//! #             bank_account.opened = true;
65//! #             bank_account.balance = balance;
66//! #         }
67//! #     }
68//! # }
69//! #
70//! #[cfg(test)]
71//! mod tests {
72//!     use thalo_testing::*;
73//!     use super::{BankAccount, BankAccountEvent};
74//!
75//!     #[test]
76//!     fn opened_account() {
77//!         BankAccount::given(
78//!             "account-123",
79//!             BankAccountEvent::OpenedAccount {
80//!                 balance: 0.0,
81//!             }
82//!         )
83//!         .should_eq(BankAccount {
84//!             id: "account-123".to_string(),
85//!             opened: true,
86//!             balance: 0.0,
87//!         });
88//!     }
89//! }
90//! ```
91//!
92//! Test aggregate commands.
93//!
94//! ```
95//! # use thalo::{
96//! #     aggregate::{Aggregate, TypeId},
97//! #     event::EventType,
98//! # };
99//! # use thiserror::Error;
100//! #
101//! # #[derive(Aggregate, Clone, Debug, Default, PartialEq, TypeId)]
102//! # struct BankAccount {
103//! #     id: String,
104//! #     opened: bool,
105//! #     balance: f64,
106//! # }
107//! #
108//! # #[derive(Clone, Debug, EventType)]
109//! # enum BankAccountEvent {
110//! #     OpenedAccount { balance: f64 },
111//! # }
112//! #
113//! # fn apply(bank_account: &mut BankAccount, event: BankAccountEvent) {
114//! #     use BankAccountEvent::*;
115//! #
116//! #     match event {
117//! #         OpenedAccount { balance } => {
118//! #             bank_account.opened = true;
119//! #             bank_account.balance = balance;
120//! #         }
121//! #     }
122//! # }
123//! #
124//! impl BankAccount {
125//!     pub fn open_account(
126//!         &self,
127//!         initial_balance: f64,
128//!     ) -> Result<BankAccountEvent, BankAccountError> {
129//!         if self.opened {
130//!             return Err(BankAccountError::AlreadyOpened);
131//!         }
132//!
133//!         if initial_balance < 0.0 {
134//!             return Err(BankAccountError::NegativeAmount);
135//!         }
136//!
137//!         Ok(BankAccountEvent::OpenedAccount {
138//!             balance: initial_balance,
139//!         })
140//!     }
141//! }
142//!
143//! #[derive(Debug, Error)]
144//! pub enum BankAccountError {
145//!     #[error("account already opened")]
146//!     AlreadyOpened,
147//!     #[error("negative amount")]
148//!     NegativeAmount,
149//! }
150//!
151//! #[cfg(test)]
152//! mod tests {
153//!     use thalo_testing::*;
154//!     use super::{BankAccount, BankAccountError, BankAccountEvent};
155//!
156//!     #[test]
157//!     fn open_account() {
158//!         BankAccount::given_no_events("account-123")
159//!             .when(|bank_account| bank_account.open_account(0.0))
160//!             .then(Ok(BankAccountEvent::OpenedAccount {
161//!                 balance: 0.0,
162//!             }));
163//!     }
164
165//!     #[test]
166//!     fn open_account_already_opened() {
167//!         BankAccount::given(
168//!             "account-123",
169//!             BankAccountEvent::OpenedAccount {
170//!                 balance: 0.0,
171//!             },
172//!         )
173//!         .when(|bank_account| bank_account.open_account(50.0))
174//!         .then(Err(BankAccountError::AlreadyOpened));
175//!     }
176//!
177//!     #[test]
178//!     fn open_account_negative_amount() {
179//!         BankAccount::given_no_events()
180//!             .when(|bank_account| bank_account.open_account(-10.0))
181//!             .then(Err(BankAccountError::NegativeAmount));
182//!     }
183//! ```
184
185#![deny(missing_docs)]
186
187use std::fmt;
188
189use thalo::{aggregate::Aggregate, event::IntoEvents};
190
191/// An aggregate given events.
192pub struct GivenTest<A>(A);
193
194/// An aggregate when a command is performed.
195pub struct WhenTest<A, R> {
196    aggregate: A,
197    result: R,
198}
199
200/// Given events for an aggregate.
201pub trait Given: Aggregate + Sized {
202    /// Given a single event for an aggregate.
203    fn given(
204        id: impl Into<<Self as Aggregate>::ID>,
205        event: impl Into<<Self as Aggregate>::Event>,
206    ) -> GivenTest<Self> {
207        Self::given_events(id, vec![event.into()])
208    }
209
210    /// Given events for an aggregate.
211    fn given_events(
212        id: impl Into<<Self as Aggregate>::ID>,
213        events: impl Into<Vec<<Self as Aggregate>::Event>>,
214    ) -> GivenTest<Self> {
215        let mut aggregate = Self::new(id.into());
216        for event in events.into() {
217            aggregate.apply(event);
218        }
219        GivenTest(aggregate)
220    }
221
222    /// Given no events for an aggregate.
223    fn given_no_events(id: impl Into<<Self as Aggregate>::ID>) -> GivenTest<Self> {
224        let aggregate = Self::new(id.into());
225        GivenTest(aggregate)
226    }
227}
228
229impl<A> Given for A where A: Aggregate + Sized {}
230
231impl<A> GivenTest<A>
232where
233    A: Aggregate,
234{
235    /// When a command is applied.
236    pub fn when<F, R>(mut self, f: F) -> WhenTest<A, R>
237    where
238        F: FnOnce(&mut A) -> R,
239    {
240        let result = f(&mut self.0);
241        WhenTest {
242            aggregate: self.0,
243            result,
244        }
245    }
246
247    /// Given previous events, the aggregate should equal the given state.
248    pub fn should_eq<S>(self, state: S) -> Self
249    where
250        A: fmt::Debug + PartialEq<S>,
251        S: fmt::Debug,
252    {
253        assert_eq!(self.0, state);
254        self
255    }
256
257    /// Given previous events, the aggregate's state should be unchanged.
258    pub fn should_be_unchanged(self) -> Self
259    where
260        A: fmt::Debug + PartialEq<A>,
261        <A as Aggregate>::ID: Clone,
262    {
263        assert_eq!(self.0, A::new(self.0.id().clone()));
264        self
265    }
266}
267
268impl<A, R> WhenTest<A, R>
269where
270    A: Aggregate,
271{
272    /// Get the inner result from the previous when() action.
273    pub fn into_result(self) -> R {
274        self.result
275    }
276
277    /// Get the inner aggregate.
278    pub fn into_state(self) -> A {
279        self.aggregate
280    }
281
282    /// Then the result of the previous when() action should equal the given parameter.
283    pub fn then<T>(self, result: T) -> WhenTest<A, R>
284    where
285        R: fmt::Debug + PartialEq<T>,
286        T: fmt::Debug,
287    {
288        assert_eq!(self.result, result);
289        self
290    }
291
292    /// When a command is applied.
293    pub fn when<F, RR>(mut self, f: F) -> WhenTest<A, RR>
294    where
295        F: FnOnce(&mut A) -> RR,
296    {
297        let result = f(&mut self.aggregate);
298        WhenTest {
299            aggregate: self.aggregate,
300            result,
301        }
302    }
303
304    /// Apply result of previous when() action.
305    pub fn apply(mut self) -> GivenTest<A>
306    where
307        R: IntoEvents<<A as Aggregate>::Event>,
308    {
309        let events = self.result.into_events();
310        for event in events {
311            self.aggregate.apply(event);
312        }
313        GivenTest(self.aggregate)
314    }
315}
316
317impl<A, R, E> WhenTest<A, Result<R, E>>
318where
319    A: Aggregate,
320{
321    /// Then the result of the previous when() action should be Ok(T), with T being equal the given parameter.
322    pub fn then_ok<T>(self, result: T) -> WhenTest<A, R>
323    where
324        T: fmt::Debug,
325        R: fmt::Debug,
326        E: fmt::Debug,
327        Result<R, E>: PartialEq<Result<T, E>>,
328    {
329        assert_eq!(self.result, Result::<T, E>::Ok(result));
330        WhenTest {
331            aggregate: self.aggregate,
332            result: self.result.unwrap(),
333        }
334    }
335
336    /// Then the result of the previous when() action should be Err(E), with E being equal the given parameter.
337    pub fn then_err<T>(self, result: T) -> GivenTest<A>
338    where
339        T: fmt::Debug,
340        R: fmt::Debug,
341        E: fmt::Debug,
342        Result<R, E>: PartialEq<Result<R, T>>,
343    {
344        assert_eq!(self.result, Result::<R, T>::Err(result));
345        GivenTest(self.aggregate)
346    }
347}