Skip to main content

eventcore_testing/
scenario.rs

1//! Given-When-Then testing helpers for eventcore commands.
2
3use eventcore_memory::InMemoryEventStore;
4use eventcore_types::{
5    CommandLogic, Event, EventStore, StreamId, StreamVersion, StreamWrites, collect_events,
6};
7
8/// Builder for Given-When-Then command tests.
9pub struct TestScenario {
10    store: InMemoryEventStore,
11}
12
13impl Default for TestScenario {
14    fn default() -> Self {
15        Self::new()
16    }
17}
18
19impl TestScenario {
20    /// Create a new test scenario with an empty event store.
21    pub fn new() -> Self {
22        Self {
23            store: InMemoryEventStore::new(),
24        }
25    }
26
27    /// Seed events into a stream as preconditions (the "Given" step).
28    pub async fn given_events<E: Event>(self, stream_id: StreamId, events: Vec<E>) -> Self {
29        if events.is_empty() {
30            return self;
31        }
32
33        let stream = self
34            .store
35            .read_stream::<E>(stream_id.clone())
36            .await
37            .expect("reading stream for test setup should not fail");
38        let existing = collect_events(stream)
39            .await
40            .expect("collecting stream for test setup should not fail");
41        let current_version = StreamVersion::new(existing.len());
42
43        let mut writes = StreamWrites::new()
44            .register_stream(stream_id, current_version)
45            .expect("registering stream for test setup should not fail");
46
47        for event in events {
48            writes = writes
49                .append(event)
50                .expect("appending event for test setup should not fail");
51        }
52
53        let _ = self
54            .store
55            .append_events(writes)
56            .await
57            .expect("appending events for test setup should not fail");
58
59        self
60    }
61
62    /// Execute a command (the "When" step). Returns a result for assertions.
63    pub async fn when<C>(self, command: C) -> ScenarioResult<C::Event>
64    where
65        C: CommandLogic,
66        C::Event: Clone + PartialEq + std::fmt::Debug,
67    {
68        let result = eventcore::execute(&self.store, command, eventcore::RetryPolicy::new()).await;
69
70        let storage: std::sync::Arc<std::sync::Mutex<Vec<C::Event>>> =
71            std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
72        let collector = crate::EventCollector::new(storage.clone());
73        let _ = eventcore::run_projection(
74            collector,
75            &self.store,
76            eventcore::ProjectionConfig::default(),
77        )
78        .await;
79
80        let all_events = storage.lock().unwrap().clone();
81
82        ScenarioResult {
83            result: result.map(|_| ()),
84            all_events,
85        }
86    }
87}
88
89/// Result of executing a command in a test scenario (the "Then" step).
90pub struct ScenarioResult<E> {
91    result: Result<(), eventcore_types::CommandError>,
92    all_events: Vec<E>,
93}
94
95impl<E: PartialEq + std::fmt::Debug> ScenarioResult<E> {
96    /// Assert the command succeeded.
97    pub fn succeeded(&self) -> &Self {
98        assert!(
99            self.result.is_ok(),
100            "expected command to succeed, got: {:?}",
101            self.result.as_ref().err()
102        );
103        self
104    }
105
106    /// Assert the command failed with a specific error.
107    ///
108    /// Accepts any error type that implements `Into<CommandError>`, matching
109    /// the same pattern used with the `require!` macro. The error is converted
110    /// to `CommandError` via `Into` and compared against the actual result.
111    pub fn failed_with<Err: Into<eventcore_types::CommandError>>(&self, expected: Err) -> &Self {
112        let expected_error = expected.into();
113        match &self.result {
114            Err(actual) => {
115                assert_eq!(
116                    actual.to_string(),
117                    expected_error.to_string(),
118                    "command error mismatch"
119                );
120            }
121            Ok(()) => panic!(
122                "expected command to fail with {}, but it succeeded",
123                expected_error
124            ),
125        }
126        self
127    }
128
129    /// Assert the events in the store match the expected list.
130    pub fn then_events(&self, expected: Vec<E>) -> &Self {
131        assert_eq!(
132            self.all_events, expected,
133            "events in store should match expected"
134        );
135        self
136    }
137
138    /// Assert the number of events in the store.
139    pub fn then_event_count(&self, expected: usize) -> &Self {
140        assert_eq!(
141            self.all_events.len(),
142            expected,
143            "expected {} events, found {}",
144            expected,
145            self.all_events.len()
146        );
147        self
148    }
149}