armature_testing/
integration.rs1use async_trait::async_trait;
6use std::future::Future;
7use std::pin::Pin;
8use std::sync::Arc;
9use thiserror::Error;
10
11pub type AsyncTestHookFn = Box<dyn Fn() -> Pin<Box<dyn Future<Output = ()> + Send>> + Send + Sync>;
13
14#[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#[async_trait]
31pub trait DatabaseTestHelper: Send + Sync {
32 async fn setup(&self) -> Result<(), IntegrationTestError>;
34
35 async fn teardown(&self) -> Result<(), IntegrationTestError>;
37
38 async fn reset(&self) -> Result<(), IntegrationTestError> {
40 self.teardown().await?;
41 self.setup().await
42 }
43
44 async fn migrate(&self) -> Result<(), IntegrationTestError> {
46 Ok(())
47 }
48
49 async fn seed(&self) -> Result<(), IntegrationTestError> {
51 Ok(())
52 }
53}
54
55pub struct TestFixture<T: DatabaseTestHelper> {
59 db_helper: Arc<T>,
60 auto_cleanup: bool,
61}
62
63impl<T: DatabaseTestHelper> TestFixture<T> {
64 pub fn new(db_helper: Arc<T>) -> Self {
72 Self {
73 db_helper,
74 auto_cleanup: true,
75 }
76 }
77
78 pub fn without_auto_cleanup(mut self) -> Self {
80 self.auto_cleanup = false;
81 self
82 }
83
84 pub async fn setup(&self) -> Result<(), IntegrationTestError> {
86 self.db_helper.setup().await
87 }
88
89 pub async fn teardown(&self) -> Result<(), IntegrationTestError> {
91 self.db_helper.teardown().await
92 }
93
94 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 self.setup().await?;
112
113 let result = test_fn().await;
115
116 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
127pub struct IntegrationTestBuilder {
129 #[allow(dead_code)]
130 name: String,
131 before_each: Vec<AsyncTestHookFn>,
132 after_each: Vec<AsyncTestHookFn>,
133}
134
135impl IntegrationTestBuilder {
136 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 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 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 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
221pub struct DatabaseSeeder {
223 fixtures: Vec<String>,
224}
225
226impl DatabaseSeeder {
227 pub fn new() -> Self {
229 Self {
230 fixtures: Vec::new(),
231 }
232 }
233
234 pub fn add_fixture(mut self, fixture: impl Into<String>) -> Self {
236 self.fixtures.push(fixture.into());
237 self
238 }
239
240 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 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}