Skip to main content

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}