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}