use async_trait::async_trait;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use thiserror::Error;
pub type AsyncTestHookFn = Box<dyn Fn() -> Pin<Box<dyn Future<Output = ()> + Send>> + Send + Sync>;
#[derive(Debug, Error)]
pub enum IntegrationTestError {
#[error("Setup failed: {0}")]
SetupFailed(String),
#[error("Teardown failed: {0}")]
TeardownFailed(String),
#[error("Database error: {0}")]
Database(String),
}
#[async_trait]
pub trait DatabaseTestHelper: Send + Sync {
async fn setup(&self) -> Result<(), IntegrationTestError>;
async fn teardown(&self) -> Result<(), IntegrationTestError>;
async fn reset(&self) -> Result<(), IntegrationTestError> {
self.teardown().await?;
self.setup().await
}
async fn migrate(&self) -> Result<(), IntegrationTestError> {
Ok(())
}
async fn seed(&self) -> Result<(), IntegrationTestError> {
Ok(())
}
}
pub struct TestFixture<T: DatabaseTestHelper> {
db_helper: Arc<T>,
auto_cleanup: bool,
}
impl<T: DatabaseTestHelper> TestFixture<T> {
pub fn new(db_helper: Arc<T>) -> Self {
Self {
db_helper,
auto_cleanup: true,
}
}
pub fn without_auto_cleanup(mut self) -> Self {
self.auto_cleanup = false;
self
}
pub async fn setup(&self) -> Result<(), IntegrationTestError> {
self.db_helper.setup().await
}
pub async fn teardown(&self) -> Result<(), IntegrationTestError> {
self.db_helper.teardown().await
}
pub async fn run_test<F, Fut>(&self, test_fn: F) -> Result<(), IntegrationTestError>
where
F: FnOnce() -> Fut,
Fut: std::future::Future<Output = Result<(), IntegrationTestError>>,
{
self.setup().await?;
let result = test_fn().await;
if self.auto_cleanup {
if let Err(e) = self.teardown().await {
eprintln!("Warning: Teardown failed: {}", e);
}
}
result
}
}
pub struct IntegrationTestBuilder {
#[allow(dead_code)]
name: String,
before_each: Vec<AsyncTestHookFn>,
after_each: Vec<AsyncTestHookFn>,
}
impl IntegrationTestBuilder {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
before_each: Vec::new(),
after_each: Vec::new(),
}
}
pub fn before_each<F, Fut>(mut self, f: F) -> Self
where
F: Fn() -> Fut + Send + Sync + 'static,
Fut: std::future::Future<Output = ()> + Send + 'static,
{
self.before_each.push(Box::new(move || Box::pin(f())));
self
}
pub fn after_each<F, Fut>(mut self, f: F) -> Self
where
F: Fn() -> Fut + Send + Sync + 'static,
Fut: std::future::Future<Output = ()> + Send + 'static,
{
self.after_each.push(Box::new(move || Box::pin(f())));
self
}
}
pub struct DatabaseSeeder {
fixtures: Vec<String>,
}
impl DatabaseSeeder {
pub fn new() -> Self {
Self {
fixtures: Vec::new(),
}
}
pub fn add_fixture(mut self, fixture: impl Into<String>) -> Self {
self.fixtures.push(fixture.into());
self
}
pub fn fixtures(&self) -> &[String] {
&self.fixtures
}
}
impl Default for DatabaseSeeder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
struct MockDbHelper;
#[async_trait]
impl DatabaseTestHelper for MockDbHelper {
async fn setup(&self) -> Result<(), IntegrationTestError> {
Ok(())
}
async fn teardown(&self) -> Result<(), IntegrationTestError> {
Ok(())
}
}
#[tokio::test]
async fn test_fixture_setup_teardown() {
let helper = Arc::new(MockDbHelper);
let fixture = TestFixture::new(helper);
fixture.setup().await.unwrap();
fixture.teardown().await.unwrap();
}
#[tokio::test]
async fn test_fixture_run_test() {
let helper = Arc::new(MockDbHelper);
let fixture = TestFixture::new(helper);
fixture
.run_test(|| async {
Ok(())
})
.await
.unwrap();
}
#[test]
fn test_database_seeder() {
let seeder = DatabaseSeeder::new()
.add_fixture("users")
.add_fixture("posts");
assert_eq!(seeder.fixtures().len(), 2);
}
}