ferro_rs/database/testing.rs
1//! Testing utilities for database operations
2//!
3//! Provides `TestDatabase` for setting up isolated test environments with
4//! in-memory SQLite databases and automatic migration support.
5//!
6//! # Example
7//!
8//! ```rust,ignore
9//! use ferro_rs::test_database;
10//!
11//! #[tokio::test]
12//! async fn test_create_user() {
13//! let db = test_database!();
14//!
15//! // Your test code here - actions using DB::connection()
16//! // will automatically use this test database
17//! }
18//! ```
19
20use sea_orm::DatabaseConnection;
21use sea_orm_migration::MigratorTrait;
22
23use super::config::DatabaseConfig;
24use super::connection::DbConnection;
25use crate::container::testing::{TestContainer, TestContainerGuard};
26use crate::error::FrameworkError;
27
28/// Test database wrapper that provides isolated database environments
29///
30/// Each `TestDatabase` creates a fresh in-memory SQLite database with
31/// migrations applied. The database is automatically registered in the
32/// test container, so any code using `DB::connection()` or `#[inject] db: Database`
33/// will receive this test database.
34///
35/// When the `TestDatabase` is dropped, the test container is cleared,
36/// ensuring complete isolation between tests.
37///
38/// # Example
39///
40/// ```rust,ignore
41/// use ferro_rs::testing::TestDatabase;
42/// use ferro_rs::migrations::Migrator;
43///
44/// #[tokio::test]
45/// async fn test_user_creation() {
46/// let db = TestDatabase::fresh::<Migrator>().await.unwrap();
47///
48/// // Actions using DB::connection() automatically get this test database
49/// let action = CreateUserAction::new();
50/// let user = action.execute("test@example.com").await.unwrap();
51///
52/// // Query directly using db.conn()
53/// let found = users::Entity::find_by_id(user.id)
54/// .one(db.conn())
55/// .await
56/// .unwrap();
57/// assert!(found.is_some());
58/// }
59/// ```
60pub struct TestDatabase {
61 conn: DbConnection,
62 _guard: TestContainerGuard,
63}
64
65impl TestDatabase {
66 /// Create a fresh test database with migrations applied
67 ///
68 /// This creates an in-memory SQLite database, runs all migrations,
69 /// and registers the connection in the test container.
70 ///
71 /// # Type Parameters
72 ///
73 /// * `M` - The migrator type implementing `MigratorTrait`. Typically
74 /// this is `crate::migrations::Migrator` from your application.
75 ///
76 /// # Errors
77 ///
78 /// Returns an error if:
79 /// - Database connection fails
80 /// - Migration execution fails
81 ///
82 /// # Example
83 ///
84 /// ```rust,ignore
85 /// use ferro_rs::testing::TestDatabase;
86 /// use ferro_rs::migrations::Migrator;
87 ///
88 /// #[tokio::test]
89 /// async fn test_example() {
90 /// let db = TestDatabase::fresh::<Migrator>().await.unwrap();
91 /// // ...
92 /// }
93 /// ```
94 pub async fn fresh<M: MigratorTrait>() -> Result<Self, FrameworkError> {
95 // 1. Create test container guard for isolation
96 let guard = TestContainer::fake();
97
98 // 2. Create in-memory SQLite database
99 let config = DatabaseConfig::builder()
100 .url("sqlite::memory:")
101 .max_connections(1)
102 .min_connections(1)
103 .logging(false)
104 .build();
105
106 let conn = DbConnection::connect(&config).await?;
107
108 // 3. Run migrations
109 M::up(conn.inner(), None)
110 .await
111 .map_err(|e| FrameworkError::database(format!("Migration failed: {e}")))?;
112
113 // 4. Register in TestContainer - this is the key integration!
114 // Any code calling DB::connection() or App::resolve::<DbConnection>()
115 // will now get this test database
116 TestContainer::singleton(conn.clone());
117
118 Ok(Self {
119 conn,
120 _guard: guard,
121 })
122 }
123
124 /// Get a reference to the underlying database connection
125 ///
126 /// Use this when you need to execute queries directly in your tests.
127 ///
128 /// # Example
129 ///
130 /// ```rust,ignore
131 /// let db = test_database!();
132 /// let users = users::Entity::find().all(db.conn()).await?;
133 /// ```
134 pub fn conn(&self) -> &DatabaseConnection {
135 self.conn.inner()
136 }
137
138 /// Get the `DbConnection` wrapper
139 ///
140 /// Use this when you need the full `DbConnection` type.
141 pub fn db(&self) -> &DbConnection {
142 &self.conn
143 }
144}
145
146/// Create a test database with default migrator
147///
148/// This macro creates a `TestDatabase` using `crate::migrations::Migrator` as the
149/// default migrator. This follows the Ferro convention where migrations are defined
150/// in `src/migrations/mod.rs`.
151///
152/// # Example
153///
154/// ```rust,ignore
155/// use ferro_rs::test_database;
156///
157/// #[tokio::test]
158/// async fn test_user_creation() {
159/// let db = test_database!();
160///
161/// let action = CreateUserAction::new();
162/// let user = action.execute("test@example.com").await.unwrap();
163/// assert!(user.id > 0);
164/// }
165/// ```
166///
167/// # With Custom Migrator
168///
169/// ```rust,ignore
170/// let db = test_database!(my_crate::CustomMigrator);
171/// ```
172#[macro_export]
173#[allow(clippy::crate_in_macro_def)]
174macro_rules! test_database {
175 () => {
176 $crate::testing::TestDatabase::fresh::<crate::migrations::Migrator>()
177 .await
178 .expect("Failed to set up test database")
179 };
180 ($migrator:ty) => {
181 $crate::testing::TestDatabase::fresh::<$migrator>()
182 .await
183 .expect("Failed to set up test database")
184 };
185}