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}