Skip to main content

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}