Skip to main content

armature_testing/
integration.rs

1//! Integration Test Helpers
2//!
3//! Provides database setup/teardown and integration test utilities.
4
5use async_trait::async_trait;
6use std::future::Future;
7use std::pin::Pin;
8use std::sync::Arc;
9use thiserror::Error;
10
11/// Type alias for async test hook function.
12pub type AsyncTestHookFn = Box<dyn Fn() -> Pin<Box<dyn Future<Output = ()> + Send>> + Send + Sync>;
13
14/// Integration test errors
15#[derive(Debug, Error)]
16pub enum IntegrationTestError {
17    #[error("Setup failed: {0}")]
18    SetupFailed(String),
19
20    #[error("Teardown failed: {0}")]
21    TeardownFailed(String),
22
23    #[error("Database error: {0}")]
24    Database(String),
25}
26
27/// Database test helper trait
28///
29/// Implement this trait for your database to provide setup/teardown.
30#[async_trait]
31pub trait DatabaseTestHelper: Send + Sync {
32    /// Setup database for testing (create tables, seed data, etc.)
33    async fn setup(&self) -> Result<(), IntegrationTestError>;
34
35    /// Teardown database after testing (drop tables, clean data, etc.)
36    async fn teardown(&self) -> Result<(), IntegrationTestError>;
37
38    /// Reset database to clean state
39    async fn reset(&self) -> Result<(), IntegrationTestError> {
40        self.teardown().await?;
41        self.setup().await
42    }
43
44    /// Run migrations
45    async fn migrate(&self) -> Result<(), IntegrationTestError> {
46        Ok(())
47    }
48
49    /// Seed test data
50    async fn seed(&self) -> Result<(), IntegrationTestError> {
51        Ok(())
52    }
53}
54
55/// Test fixture
56///
57/// Manages test lifecycle with automatic setup/teardown.
58pub struct TestFixture<T: DatabaseTestHelper> {
59    db_helper: Arc<T>,
60    auto_cleanup: bool,
61}
62
63impl<T: DatabaseTestHelper> TestFixture<T> {
64    /// Create new test fixture
65    ///
66    /// # Examples
67    ///
68    /// ```rust,ignore
69    /// let fixture = TestFixture::new(Arc::new(MyDbHelper::new()));
70    /// ```
71    pub fn new(db_helper: Arc<T>) -> Self {
72        Self {
73            db_helper,
74            auto_cleanup: true,
75        }
76    }
77
78    /// Disable automatic cleanup
79    pub fn without_auto_cleanup(mut self) -> Self {
80        self.auto_cleanup = false;
81        self
82    }
83
84    /// Run setup
85    pub async fn setup(&self) -> Result<(), IntegrationTestError> {
86        self.db_helper.setup().await
87    }
88
89    /// Run teardown
90    pub async fn teardown(&self) -> Result<(), IntegrationTestError> {
91        self.db_helper.teardown().await
92    }
93
94    /// Run test with automatic setup/teardown
95    ///
96    /// # Examples
97    ///
98    /// ```rust,ignore
99    /// fixture.run_test(|| async {
100    ///     // Your test code here
101    ///     // Database is automatically set up before and torn down after
102    ///     Ok(())
103    /// }).await?;
104    /// ```
105    pub async fn run_test<F, Fut>(&self, test_fn: F) -> Result<(), IntegrationTestError>
106    where
107        F: FnOnce() -> Fut,
108        Fut: std::future::Future<Output = Result<(), IntegrationTestError>>,
109    {
110        // Setup
111        self.setup().await?;
112
113        // Run test
114        let result = test_fn().await;
115
116        // Teardown (even if test failed)
117        if self.auto_cleanup
118            && let Err(e) = self.teardown().await
119        {
120            eprintln!("Warning: Teardown failed: {}", e);
121        }
122
123        result
124    }
125}
126
127/// Integration test builder
128pub struct IntegrationTestBuilder {
129    #[allow(dead_code)]
130    name: String,
131    before_each: Vec<AsyncTestHookFn>,
132    after_each: Vec<AsyncTestHookFn>,
133}
134
135impl IntegrationTestBuilder {
136    /// Create new integration test builder
137    pub fn new(name: impl Into<String>) -> Self {
138        Self {
139            name: name.into(),
140            before_each: Vec::new(),
141            after_each: Vec::new(),
142        }
143    }
144
145    /// Add before_each hook
146    pub fn before_each<F, Fut>(mut self, f: F) -> Self
147    where
148        F: Fn() -> Fut + Send + Sync + 'static,
149        Fut: std::future::Future<Output = ()> + Send + 'static,
150    {
151        self.before_each.push(Box::new(move || Box::pin(f())));
152        self
153    }
154
155    /// Add after_each hook
156    pub fn after_each<F, Fut>(mut self, f: F) -> Self
157    where
158        F: Fn() -> Fut + Send + Sync + 'static,
159        Fut: std::future::Future<Output = ()> + Send + 'static,
160    {
161        self.after_each.push(Box::new(move || Box::pin(f())));
162        self
163    }
164
165    /// Run `body` with the collected hooks around it: every `before_each`
166    /// hook is awaited (in registration order), then `body`, then every
167    /// `after_each` hook (in registration order). `after_each` hooks run
168    /// whenever `body` returns normally; if `body` panics, the panic
169    /// unwinds through this function and `after_each` hooks do *not* run
170    /// (matching `TestFixture::run_test`'s non-catching behavior).
171    ///
172    /// # Examples
173    ///
174    /// ```
175    /// use armature_testing::integration::IntegrationTestBuilder;
176    /// use std::sync::Arc;
177    /// use std::sync::atomic::{AtomicUsize, Ordering};
178    ///
179    /// # tokio_test::block_on(async {
180    /// let order = Arc::new(AtomicUsize::new(0));
181    /// let before_slot = order.clone();
182    /// let after_slot = order.clone();
183    ///
184    /// let builder = IntegrationTestBuilder::new("example")
185    ///     .before_each(move || {
186    ///         let before_slot = before_slot.clone();
187    ///         async move { before_slot.store(1, Ordering::SeqCst); }
188    ///     })
189    ///     .after_each(move || {
190    ///         let after_slot = after_slot.clone();
191    ///         async move { after_slot.store(3, Ordering::SeqCst); }
192    ///     });
193    ///
194    /// let seen_during_body = builder
195    ///     .run_test(|| async { order.load(Ordering::SeqCst) })
196    ///     .await;
197    ///
198    /// assert_eq!(seen_during_body, 1); // before_each already ran
199    /// assert_eq!(order.load(Ordering::SeqCst), 3); // after_each ran too
200    /// # });
201    /// ```
202    pub async fn run_test<F, Fut, T>(&self, body: F) -> T
203    where
204        F: FnOnce() -> Fut,
205        Fut: std::future::Future<Output = T>,
206    {
207        for hook in &self.before_each {
208            hook().await;
209        }
210
211        let result = body().await;
212
213        for hook in &self.after_each {
214            hook().await;
215        }
216
217        result
218    }
219}
220
221/// Database seeder
222pub struct DatabaseSeeder {
223    fixtures: Vec<String>,
224}
225
226impl DatabaseSeeder {
227    /// Create new database seeder
228    pub fn new() -> Self {
229        Self {
230            fixtures: Vec::new(),
231        }
232    }
233
234    /// Add fixture
235    pub fn add_fixture(mut self, fixture: impl Into<String>) -> Self {
236        self.fixtures.push(fixture.into());
237        self
238    }
239
240    /// Get all fixtures
241    pub fn fixtures(&self) -> &[String] {
242        &self.fixtures
243    }
244}
245
246impl Default for DatabaseSeeder {
247    fn default() -> Self {
248        Self::new()
249    }
250}
251
252#[cfg(test)]
253mod tests {
254    use super::*;
255
256    struct MockDbHelper;
257
258    #[async_trait]
259    impl DatabaseTestHelper for MockDbHelper {
260        async fn setup(&self) -> Result<(), IntegrationTestError> {
261            Ok(())
262        }
263
264        async fn teardown(&self) -> Result<(), IntegrationTestError> {
265            Ok(())
266        }
267    }
268
269    #[tokio::test]
270    async fn test_fixture_setup_teardown() {
271        let helper = Arc::new(MockDbHelper);
272        let fixture = TestFixture::new(helper);
273
274        fixture.setup().await.unwrap();
275        fixture.teardown().await.unwrap();
276    }
277
278    #[tokio::test]
279    async fn test_fixture_run_test() {
280        let helper = Arc::new(MockDbHelper);
281        let fixture = TestFixture::new(helper);
282
283        fixture
284            .run_test(|| async {
285                // Test code
286                Ok(())
287            })
288            .await
289            .unwrap();
290    }
291
292    #[test]
293    fn test_database_seeder() {
294        let seeder = DatabaseSeeder::new()
295            .add_fixture("users")
296            .add_fixture("posts");
297
298        assert_eq!(seeder.fixtures().len(), 2);
299    }
300
301    #[tokio::test]
302    async fn run_test_invokes_hooks_in_order_around_body() {
303        let log = Arc::new(std::sync::Mutex::new(Vec::<&'static str>::new()));
304
305        let before_log = log.clone();
306        let before_log_2 = log.clone();
307        let after_log = log.clone();
308        let after_log_2 = log.clone();
309
310        let builder = IntegrationTestBuilder::new("ordering")
311            .before_each(move || {
312                let log = before_log.clone();
313                async move { log.lock().unwrap().push("before_1") }
314            })
315            .before_each(move || {
316                let log = before_log_2.clone();
317                async move { log.lock().unwrap().push("before_2") }
318            })
319            .after_each(move || {
320                let log = after_log.clone();
321                async move { log.lock().unwrap().push("after_1") }
322            })
323            .after_each(move || {
324                let log = after_log_2.clone();
325                async move { log.lock().unwrap().push("after_2") }
326            });
327
328        let body_log = log.clone();
329        builder
330            .run_test(|| async move {
331                body_log.lock().unwrap().push("body");
332            })
333            .await;
334
335        assert_eq!(
336            *log.lock().unwrap(),
337            vec!["before_1", "before_2", "body", "after_1", "after_2"]
338        );
339    }
340
341    #[tokio::test]
342    async fn run_test_returns_the_body_result() {
343        let builder = IntegrationTestBuilder::new("passthrough");
344        let value = builder.run_test(|| async { 42 }).await;
345        assert_eq!(value, 42);
346    }
347
348    #[tokio::test]
349    async fn run_test_runs_after_each_even_when_no_before_each_hooks_registered() {
350        let ran_after = Arc::new(std::sync::atomic::AtomicBool::new(false));
351        let flag = ran_after.clone();
352
353        let builder = IntegrationTestBuilder::new("no-before").after_each(move || {
354            let flag = flag.clone();
355            async move {
356                flag.store(true, std::sync::atomic::Ordering::SeqCst);
357            }
358        });
359
360        builder.run_test(|| async {}).await;
361        assert!(ran_after.load(std::sync::atomic::Ordering::SeqCst));
362    }
363}