Skip to main content

armature_testing/
mock.rs

1// Mock utilities for testing
2
3use armature_core::Provider;
4use std::sync::{Arc, Mutex};
5
6/// Mock service for testing
7#[derive(Clone)]
8pub struct MockService<T> {
9    calls: Arc<Mutex<Vec<String>>>,
10    return_value: Arc<Mutex<Option<T>>>,
11}
12
13impl<T> MockService<T> {
14    /// Create a new mock service
15    pub fn new() -> Self {
16        Self {
17            calls: Arc::new(Mutex::new(Vec::new())),
18            return_value: Arc::new(Mutex::new(None)),
19        }
20    }
21
22    /// Set the return value
23    pub fn with_return(self, value: T) -> Self {
24        *self.return_value.lock().unwrap() = Some(value);
25        self
26    }
27
28    /// Record a method call
29    pub fn record_call(&self, method: &str) {
30        self.calls.lock().unwrap().push(method.to_string());
31    }
32
33    /// Get the number of calls
34    pub fn call_count(&self) -> usize {
35        self.calls.lock().unwrap().len()
36    }
37
38    /// Get all recorded calls
39    pub fn get_calls(&self) -> Vec<String> {
40        self.calls.lock().unwrap().clone()
41    }
42
43    /// Check if a method was called
44    pub fn was_called(&self, method: &str) -> bool {
45        self.calls.lock().unwrap().contains(&method.to_string())
46    }
47
48    /// Clear all recorded calls
49    pub fn clear_calls(&self) {
50        self.calls.lock().unwrap().clear();
51    }
52
53    /// Get the mock return value
54    pub fn get_return(&self) -> Option<T>
55    where
56        T: Clone,
57    {
58        self.return_value.lock().unwrap().clone()
59    }
60}
61
62impl<T> Default for MockService<T> {
63    fn default() -> Self {
64        Self::new()
65    }
66}
67
68type CallLog = Arc<Mutex<Vec<(String, Vec<String>)>>>;
69
70/// Mock controller for testing
71#[derive(Clone)]
72pub struct MockController {
73    _name: String,
74    calls: CallLog,
75}
76
77impl MockController {
78    /// Create a new mock controller
79    pub fn new(name: &str) -> Self {
80        Self {
81            _name: name.to_string(),
82            calls: Arc::new(Mutex::new(Vec::new())),
83        }
84    }
85
86    /// Record a method call with arguments
87    pub fn record_call(&self, method: &str, args: Vec<String>) {
88        self.calls.lock().unwrap().push((method.to_string(), args));
89    }
90
91    /// Get the number of calls to a specific method
92    pub fn method_call_count(&self, method: &str) -> usize {
93        self.calls
94            .lock()
95            .unwrap()
96            .iter()
97            .filter(|(m, _)| m == method)
98            .count()
99    }
100
101    /// Get all calls
102    pub fn get_all_calls(&self) -> Vec<(String, Vec<String>)> {
103        self.calls.lock().unwrap().clone()
104    }
105
106    /// Clear all calls
107    pub fn clear(&self) {
108        self.calls.lock().unwrap().clear();
109    }
110}
111
112/// Mock provider trait implementation
113pub trait MockProvider: Provider + Clone {
114    /// Reset the mock to its initial state
115    fn reset(&mut self);
116
117    /// Get the number of calls
118    fn call_count(&self) -> usize;
119}
120
121/// Call recorder that carries a wrapped value alongside it.
122///
123/// # This does not intercept anything
124///
125/// `Spy` is a **manual** recorder, not a proxy. Wrapping a value does not make
126/// its methods observable: [`Spy::inner`] hands back the value untouched, and
127/// calls made through that reference go straight to it. Recording happens only
128/// where you call [`Spy::record`] yourself:
129///
130/// ```
131/// # use armature_testing::Spy;
132/// #[derive(Clone)]
133/// struct Repo;
134/// impl Repo {
135///     fn find(&self, id: u32) -> String { format!("user-{id}") }
136/// }
137///
138/// let spy = Spy::new(Repo);
139///
140/// // The call is recorded because the test records it, not because `Spy` saw it.
141/// spy.record("find");
142/// let user = spy.inner().find(7);
143///
144/// assert_eq!(user, "user-7");
145/// assert_eq!(spy.call_count(), 1);
146/// assert!(spy.was_called("find"));
147/// ```
148///
149/// For automatic interception, implement the trait under test on a hand-written
150/// fake that records in each method - `Spy` is then useful as that fake's
151/// storage.
152///
153/// Cloning a `Spy` shares the recording buffer, so a clone handed to production
154/// code records into the same log the test asserts on.
155#[derive(Clone)]
156pub struct Spy<T: Clone> {
157    inner: T,
158    calls: Arc<Mutex<Vec<String>>>,
159}
160
161impl<T: Clone> Spy<T> {
162    /// Create a new spy wrapping a provider
163    pub fn new(inner: T) -> Self {
164        Self {
165            inner,
166            calls: Arc::new(Mutex::new(Vec::new())),
167        }
168    }
169
170    /// Record a method call.
171    ///
172    /// Call this yourself at each site you want observed; `Spy` cannot see
173    /// calls made through [`Spy::inner`].
174    pub fn record(&self, method: &str) {
175        self.calls.lock().unwrap().push(method.to_string());
176    }
177
178    /// Get the number of recorded calls
179    pub fn call_count(&self) -> usize {
180        self.calls.lock().unwrap().len()
181    }
182
183    /// Check if a method was recorded as called
184    pub fn was_called(&self, method: &str) -> bool {
185        self.calls.lock().unwrap().contains(&method.to_string())
186    }
187
188    /// Every recorded call, in order.
189    pub fn calls(&self) -> Vec<String> {
190        self.calls.lock().unwrap().clone()
191    }
192
193    /// Drop every recorded call.
194    pub fn clear(&self) {
195        self.calls.lock().unwrap().clear();
196    }
197
198    /// Get the wrapped provider.
199    ///
200    /// Returned untouched - calls made through this reference are NOT
201    /// recorded. See the type-level docs.
202    pub fn inner(&self) -> &T {
203        &self.inner
204    }
205}
206
207#[cfg(test)]
208mod tests {
209    use super::*;
210
211    #[test]
212    fn test_mock_service() {
213        let mock = MockService::<String>::new();
214        mock.record_call("test_method");
215        assert_eq!(mock.call_count(), 1);
216        assert!(mock.was_called("test_method"));
217    }
218
219    #[test]
220    fn test_mock_controller() {
221        let mock = MockController::new("TestController");
222        mock.record_call("get", vec!["id".to_string()]);
223        assert_eq!(mock.method_call_count("get"), 1);
224    }
225
226    #[test]
227    fn test_spy() {
228        let spy = Spy::new("test_value");
229        spy.record("method1");
230        spy.record("method2");
231        assert_eq!(spy.call_count(), 2);
232        assert!(spy.was_called("method1"));
233    }
234}