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