reinhardt_testkit/fixtures/mock.rs
1use mockall::mock;
2use reinhardt_db::backends::{
3 Result,
4 backend::DatabaseBackend as BackendTrait,
5 connection::DatabaseConnection as BackendsConnection,
6 types::{DatabaseType, QueryResult, QueryValue, Row, TransactionExecutor},
7};
8use reinhardt_db::orm::{DatabaseBackend, DatabaseConnection};
9use rstest::*;
10use std::sync::Arc;
11
12// ============================================================================
13// mockall-based Database Backend Mock
14// ============================================================================
15
16mock! {
17 /// Mock implementation of DatabaseBackend trait using mockall
18 ///
19 /// This mock provides automatic verification of method calls and arguments.
20 ///
21 /// # Usage with rstest Fixtures
22 ///
23 /// For complete examples using rstest fixtures, see the unit tests in this module:
24 /// - `test_mock_execute_with_verification()` - Demonstrates strict argument verification
25 /// - `test_with_mock_database()` - Shows usage with the `mock_database` fixture
26 /// - `test_with_mock_connection()` - Shows usage with the `mock_connection` fixture
27 ///
28 /// # Direct Usage Example
29 ///
30 /// ```rust
31 /// use reinhardt_testkit::fixtures::MockDatabaseBackend;
32 /// use reinhardt_db::backends::types::{QueryResult, QueryValue};
33 /// use reinhardt_db::backends::backend::DatabaseBackend as BackendTrait;
34 ///
35 /// #[tokio::main]
36 /// async fn main() {
37 /// let mut mock = MockDatabaseBackend::new();
38 ///
39 /// // Set expectations with strict argument verification
40 /// mock.expect_execute()
41 /// .withf(|sql, params| {
42 /// sql.contains("INSERT INTO users") && params.len() == 2
43 /// })
44 /// .times(1)
45 /// .returning(|_, _| Ok(QueryResult { rows_affected: 1 }));
46 ///
47 /// // Execute the query (must call to satisfy .times(1) expectation)
48 /// let result = mock.execute(
49 /// "INSERT INTO users (name, email) VALUES ($1, $2)",
50 /// vec![
51 /// QueryValue::String("Alice".to_string()),
52 /// QueryValue::String("alice@example.com".to_string()),
53 /// ],
54 /// ).await;
55 ///
56 /// assert!(result.is_ok());
57 /// // Mock automatically verifies expectations on drop
58 /// }
59 /// ```
60 pub DatabaseBackend {}
61
62 #[async_trait::async_trait]
63 impl BackendTrait for DatabaseBackend {
64 fn database_type(&self) -> DatabaseType;
65 fn placeholder(&self, index: usize) -> String;
66 fn supports_returning(&self) -> bool;
67 fn supports_on_conflict(&self) -> bool;
68
69 async fn execute(&self, sql: &str, params: Vec<QueryValue>) -> Result<QueryResult>;
70 async fn fetch_one(&self, sql: &str, params: Vec<QueryValue>) -> Result<Row>;
71 async fn fetch_all(&self, sql: &str, params: Vec<QueryValue>) -> Result<Vec<Row>>;
72 async fn fetch_optional(&self, sql: &str, params: Vec<QueryValue>) -> Result<Option<Row>>;
73 async fn begin(&self) -> Result<Box<dyn TransactionExecutor>>;
74
75 fn as_any(&self) -> &dyn std::any::Any;
76 }
77}
78
79// SAFETY: MockDatabaseBackend is Send-safe because all internal state
80// (mockall expectations) is stored in thread-safe containers. The mock
81// is designed for single-threaded test usage with tokio's async runtime,
82// and expectations are set before any concurrent access occurs.
83unsafe impl Send for MockDatabaseBackend {}
84// SAFETY: MockDatabaseBackend is Sync-safe because all expectation
85// matching in mockall uses internal synchronization. The mock backend
86// is accessed through Arc<MockDatabaseBackend> in test fixtures, and
87// concurrent read access to expectations is safe.
88unsafe impl Sync for MockDatabaseBackend {}
89
90// ============================================================================
91// rstest Fixtures
92// ============================================================================
93
94/// Fixture providing a mock database backend with default expectations
95///
96/// This fixture creates a MockDatabaseBackend with basic default behaviors:
97/// - PostgreSQL database type
98/// - Standard $N placeholder format
99/// - All optional features supported (RETURNING, ON CONFLICT)
100///
101/// # Usage with rstest
102///
103/// This fixture is designed to be used with rstest's `#[rstest]` attribute.
104/// See the unit tests in this module for complete examples:
105/// - `test_mock_database_default_expectations()` - Verifies default expectations
106/// - `test_mock_execute_with_verification()` - Shows strict argument verification
107///
108/// Note: Doctests cannot use rstest fixtures directly due to Rust's doctest limitations.
109/// For runnable examples, refer to the unit tests in the `#[cfg(test)]` section below.
110#[fixture]
111pub fn mock_database() -> MockDatabaseBackend {
112 let mut mock = MockDatabaseBackend::new();
113
114 // Default expectations
115 mock.expect_database_type()
116 .return_const(DatabaseType::Postgres);
117
118 mock.expect_placeholder()
119 .returning(|idx| format!("${}", idx));
120
121 mock.expect_supports_returning().return_const(true);
122
123 mock.expect_supports_on_conflict().return_const(true);
124
125 // Note: as_any() expectation intentionally not set
126 // It will panic if called, which is the desired behavior for tests
127
128 mock
129}
130
131/// Fixture providing a complete DatabaseConnection with mock backend
132///
133/// This fixture creates a fully configured DatabaseConnection with a mock backend
134/// that returns empty results by default. Suitable for unit tests that only need
135/// to verify connection-level behavior without actual database operations.
136///
137/// # Usage with rstest
138///
139/// This fixture is designed to be used with rstest's `#[rstest]` attribute.
140/// See the unit test `test_mock_connection_fixture()` for a complete example.
141///
142/// Note: Doctests cannot use rstest fixtures directly due to Rust's doctest limitations.
143/// For runnable examples, refer to the unit tests in the `#[cfg(test)]` section below.
144#[fixture]
145pub fn mock_connection() -> DatabaseConnection {
146 let mut mock = MockDatabaseBackend::new();
147
148 // Basic configuration
149 mock.expect_database_type()
150 .return_const(DatabaseType::Postgres);
151
152 mock.expect_placeholder()
153 .returning(|idx| format!("${}", idx));
154
155 mock.expect_supports_returning().return_const(true);
156
157 mock.expect_supports_on_conflict().return_const(true);
158
159 // Note: as_any() expectation intentionally not set
160 // It will panic if called, which is the desired behavior for tests
161
162 // Default query behavior: return empty results
163 mock.expect_execute()
164 .returning(|_, _| Ok(QueryResult { rows_affected: 0 }));
165
166 mock.expect_fetch_all().returning(|_, _| Ok(Vec::new()));
167
168 mock.expect_fetch_one().returning(|_, _| {
169 let mut row = Row::new();
170 row.data.insert("count".to_string(), QueryValue::Int(0));
171 Ok(row)
172 });
173
174 mock.expect_fetch_optional().returning(|_, _| Ok(None));
175
176 let backends_conn = BackendsConnection::new(Arc::new(mock));
177 DatabaseConnection::new(DatabaseBackend::Postgres, backends_conn)
178}
179
180#[cfg(test)]
181mod tests {
182 use super::*;
183
184 #[test]
185 fn test_mock_database_default_expectations() {
186 let mock = mock_database();
187
188 assert_eq!(mock.database_type(), DatabaseType::Postgres);
189 assert_eq!(mock.placeholder(1), "$1");
190 assert!(mock.supports_returning());
191 assert!(mock.supports_on_conflict());
192 }
193
194 #[test]
195 fn test_mock_database_custom_expectations() {
196 let mut mock = MockDatabaseBackend::new();
197
198 mock.expect_database_type()
199 .return_const(DatabaseType::Mysql);
200
201 mock.expect_placeholder().returning(|_| "?".to_string());
202
203 assert_eq!(mock.database_type(), DatabaseType::Mysql);
204 assert_eq!(mock.placeholder(1), "?");
205 }
206
207 #[tokio::test]
208 async fn test_mock_execute_with_verification() {
209 let mut mock = MockDatabaseBackend::new();
210
211 // Strict verification: exact SQL and param count
212 mock.expect_execute()
213 .withf(|sql, params| sql.contains("INSERT INTO users") && params.len() == 2)
214 .times(1)
215 .returning(|_, _| Ok(QueryResult { rows_affected: 1 }));
216
217 let result = mock
218 .execute(
219 "INSERT INTO users (name, email) VALUES ($1, $2)",
220 vec![
221 QueryValue::String("Alice".to_string()),
222 QueryValue::String("alice@example.com".to_string()),
223 ],
224 )
225 .await;
226
227 assert!(result.is_ok());
228 assert_eq!(result.unwrap().rows_affected, 1);
229
230 // Mock automatically verifies that .times(1) expectation was met on drop
231 }
232
233 #[rstest]
234 fn test_mock_connection_fixture(mock_connection: DatabaseConnection) {
235 // Verify connection is usable
236 assert!(matches!(
237 mock_connection.backend(),
238 DatabaseBackend::Postgres
239 ));
240 }
241}