testkit_core/
lib.rs

1mod context;
2mod handlers;
3mod testdb;
4mod tracing;
5pub mod utils;
6
7// Re-exported types and traits
8pub use context::*;
9pub use handlers::*;
10pub use testdb::*;
11pub use tracing::*;
12pub use utils::*;
13
14// The boxed_async macro is already exported with #[macro_export]
15
16use std::{fmt::Debug, pin::Pin};
17
18/// A test context that contains a database instance
19#[derive(Clone)]
20pub struct TestContext<DB>
21where
22    DB: DatabaseBackend + Send + Sync + Debug + 'static,
23{
24    pub db: TestDatabaseInstance<DB>,
25}
26
27impl<DB> Debug for TestContext<DB>
28where
29    DB: DatabaseBackend + Send + Sync + Debug + 'static,
30{
31    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32        write!(f, "TestContext {{ db: {:?} }}", self.db.db_name)
33    }
34}
35
36impl<DB> TestContext<DB>
37where
38    DB: DatabaseBackend + Send + Sync + Debug + 'static,
39{
40    pub fn new(db: TestDatabaseInstance<DB>) -> Self {
41        Self { db }
42    }
43}
44
45/// Testing utilities for working with database handlers in a mock environment
46///
47/// This module provides ergonomic APIs for working with database tests, allowing
48/// you to create seamless test interactions with databases.
49///
50/// # Examples
51///
52/// Using the direct API with `setup_async` and `transaction` methods:
53///
54/// ```rust,no_run
55/// use testkit_core::*;
56///
57/// #[tokio::test]
58/// async fn test_database() {
59///     let backend = MockBackend::new();
60///    
61///     // Direct API with setup_async and transaction methods (no boxed_async needed)
62///     let ctx = with_boxed_database(backend)
63///         .setup_async(|conn| async {
64///             println!("Setting up database");
65///             Ok(())
66///         })
67///         .transaction(|conn| async {
68///             println!("Running transaction");
69///             Ok(())
70///         })
71///         .run()
72///         .await
73///         .expect("Test failed");
74/// }
75/// ```
76///
77/// Using the `db_test!` macro for a clean entry point:
78///
79/// ```rust,no_run
80/// use testkit_core::*;
81///
82/// #[tokio::test]
83/// async fn test_with_macro() {
84///     let backend = MockBackend::new();
85///     
86///     // Variable capture works seamlessly
87///     let table_name = "users".to_string();
88///    
89///     // Using db_test! macro as a more readable entry point
90///     let ctx = db_test!(backend)
91///         .setup_async(|conn| async move {
92///             println!("Creating table: {}", table_name);
93///             Ok(())
94///         })
95///         .transaction(|conn| async {
96///             println!("Running transaction");
97///             Ok(())
98///         })
99///         .run()
100///         .await
101///         .expect("Test failed");
102/// }
103/// ```
104///
105/// For more examples, check the `tests/ergonomic_api.rs` file.
106pub mod tests {
107    pub mod mock {
108        // A minimal mock backend for testing
109        use async_trait::async_trait;
110        use std::fmt::Debug;
111
112        use crate::{
113            DatabaseBackend, DatabaseConfig, DatabaseName, DatabasePool, TestDatabaseConnection,
114        };
115
116        // Define a mock connection type
117        #[derive(Debug, Clone)]
118        pub struct MockConnection;
119
120        impl TestDatabaseConnection for MockConnection {
121            fn connection_string(&self) -> String {
122                "mock://test".to_string()
123            }
124        }
125
126        // Define a mock pool type
127        #[derive(Debug, Clone)]
128        pub struct MockPool;
129
130        #[async_trait]
131        impl DatabasePool for MockPool {
132            type Connection = MockConnection;
133            type Error = MockError;
134
135            async fn acquire(&self) -> Result<Self::Connection, Self::Error> {
136                Ok(MockConnection)
137            }
138
139            async fn release(&self, _conn: Self::Connection) -> Result<(), Self::Error> {
140                Ok(())
141            }
142
143            fn connection_string(&self) -> String {
144                "mock://test".to_string()
145            }
146        }
147
148        // Define a simple error type
149        #[derive(Debug, Clone)]
150        pub struct MockError(pub String);
151
152        impl std::fmt::Display for MockError {
153            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
154                write!(f, "MockError: {}", self.0)
155            }
156        }
157
158        impl std::error::Error for MockError {}
159
160        impl From<String> for MockError {
161            fn from(s: String) -> Self {
162                MockError(s)
163            }
164        }
165
166        // Define a mock backend
167        #[derive(Debug, Clone, Default)]
168        pub struct MockBackend;
169
170        #[async_trait]
171        impl DatabaseBackend for MockBackend {
172            type Connection = MockConnection;
173            type Pool = MockPool;
174            type Error = MockError;
175
176            async fn new(_config: DatabaseConfig) -> Result<Self, Self::Error> {
177                Ok(Self)
178            }
179
180            async fn connect(&self, _name: &DatabaseName) -> Result<Self::Connection, Self::Error> {
181                Ok(MockConnection)
182            }
183
184            async fn connect_with_string(
185                &self,
186                _connection_string: &str,
187            ) -> Result<Self::Connection, Self::Error> {
188                Ok(MockConnection)
189            }
190
191            async fn create_pool(
192                &self,
193                _name: &DatabaseName,
194                _config: &DatabaseConfig,
195            ) -> Result<Self::Pool, Self::Error> {
196                Ok(MockPool)
197            }
198
199            async fn create_database(
200                &self,
201                _pool: &Self::Pool,
202                _name: &DatabaseName,
203            ) -> Result<(), Self::Error> {
204                Ok(())
205            }
206
207            fn drop_database(&self, name: &DatabaseName) -> Result<(), Self::Error> {
208                // In a mock implementation, log that we would drop the database
209                tracing::info!("Mock dropping database: {}", name);
210                Ok(())
211            }
212
213            fn connection_string(&self, _name: &DatabaseName) -> String {
214                "mock://database".to_string()
215            }
216        }
217    }
218}
219
220/// Execute a function with a newly created connection and automatically close it
221///
222/// This function creates a connection to the database with the given name using the
223/// provided backend, then executes the operation with that connection. The connection
224/// is automatically closed when the operation completes.
225///
226/// This is the most efficient way to perform a one-off database operation without
227/// the overhead of creating and managing a connection pool.
228///
229/// # Example
230/// ```rust,no_run
231/// use testkit_core::{with_connection, DatabaseBackend, DatabaseName, boxed_async};
232/// use std::fmt::{Display, Formatter};
233///
234/// // Define a custom error type for our example
235/// #[derive(Debug)]
236/// struct ExampleError(String);
237///
238/// impl std::error::Error for ExampleError {}
239///
240/// impl Display for ExampleError {
241///     fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
242///         write!(f, "{}", self.0)
243///     }
244/// }
245///
246/// async fn example<B: DatabaseBackend>(backend: B, name: &DatabaseName) -> Result<(), B::Error>
247/// where B::Error: From<ExampleError> {
248///     with_connection(backend, name, |conn| boxed_async!(async move {
249///         // Perform operations with the connection
250///         Ok::<(), ExampleError>(())
251///     })).await
252/// }
253/// ```
254pub async fn with_connection<B, F, R, E>(
255    backend: B,
256    name: &DatabaseName,
257    operation: F,
258) -> Result<R, B::Error>
259where
260    B: DatabaseBackend,
261    F: FnOnce(&B::Connection) -> Pin<Box<dyn Future<Output = Result<R, E>> + Send>> + Send,
262    E: std::error::Error + Send + Sync + 'static,
263    B::Error: From<E>,
264{
265    // Create a connection
266    let conn = backend.connect(name).await?;
267
268    // Run the operation
269    let result = operation(&conn).await.map_err(|e| B::Error::from(e))?;
270
271    // Connection will be dropped automatically when it goes out of scope
272    Ok(result)
273}
274
275/// Execute a function with a newly created connection using a connection string
276///
277/// This function creates a connection to the database using the provided connection string
278/// and backend, then executes the operation with that connection. The connection is
279/// automatically closed when the operation completes.
280///
281/// This is the most efficient way to perform a one-off database operation without
282/// the overhead of creating and managing a connection pool.
283///
284/// # Example
285/// ```rust,no_run
286/// use testkit_core::{with_connection_string, DatabaseBackend, boxed_async};
287/// use std::fmt::{Display, Formatter};
288///
289/// // Define a custom error type for our example
290/// #[derive(Debug)]
291/// struct ExampleError(String);
292///
293/// impl std::error::Error for ExampleError {}
294///
295/// impl Display for ExampleError {
296///     fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
297///         write!(f, "{}", self.0)
298///     }
299/// }
300///
301/// async fn example<B: DatabaseBackend>(backend: B, connection_string: &str) -> Result<(), B::Error>
302/// where B::Error: From<ExampleError> {
303///     with_connection_string(backend, connection_string, |conn| boxed_async!(async move {
304///         // Perform operations with the connection
305///         Ok::<(), ExampleError>(())
306///     })).await
307/// }
308/// ```
309pub async fn with_connection_string<B, F, R, E>(
310    backend: B,
311    connection_string: &str,
312    operation: F,
313) -> Result<R, B::Error>
314where
315    B: DatabaseBackend,
316    F: FnOnce(&B::Connection) -> Pin<Box<dyn Future<Output = Result<R, E>> + Send>> + Send,
317    E: std::error::Error + Send + Sync + 'static,
318    B::Error: From<E>,
319{
320    // Create a connection using the connection string
321    let conn = backend.connect_with_string(connection_string).await?;
322
323    // Run the operation
324    let result = operation(&conn).await.map_err(|e| B::Error::from(e))?;
325
326    // Connection will be dropped automatically when it goes out of scope
327    Ok(result)
328}