teststack-macros 0.1.0

Test utilities to run testcontainers
Documentation
# Teststack

Teststack is a Rust utility crate that simplifies the setup and management of reusable test containers using the  [testcontainers](https://docs.rs/testcontainers/latest/testcontainers/) library. It starts each container once per test suite and shares it across tests, reducing overhead and complexity.

# Features

- Singleton containers per image type across tests
- Automatic cleanup at the end of the test run (or on Ctrl+C)
- Pluggable container support: PostgreSQL, MySQL, and custom containers
- Easy integration with test frameworks (sqlx::test, rstest)

# Getting Started

Add `teststack` to your Cargo.toml:

```toml
[dev-dependencies]
teststack = { version = "0.1", features = ["postgres"] }
```

# Example: Shared Container Across Tests

In the example below, both tests share the same `Postgres` container instance. This reduces startup overhead and speeds up test execution. The container is automatically shut down at the end of the test harness.

```rust
use sqlx::{MySqlPool, PgPool};
use teststack::stack;

#[stack(postgres(random_db_name))]
#[sqlx::test]
async fn test_postgres(pool: PgPool) {
    sqlx::query("SELECT 1")
        .fetch_one(&pool)
        .await
        .expect("failed to execute query");
}

#[stack(postgres(random_db_name))]
#[sqlx::test]
async fn test_postgres_2(pool: PgPool) {
    sqlx::query("SELECT 1")
        .fetch_one(&pool)
        .await
        .expect("failed to execute query");
}
```

# Custom Containers and Configuration

The following example demonstrates how to run a `RabbitMq` container using teststack. It shows how to customize an existing `testcontainers_module` image with the `ImageExt` trait for advanced configuration.

```rust
use testcontainers_modules::rabbitmq::RabbitMq;
use testcontainers_modules::testcontainers::{ContainerRequest, ImageExt};
use teststack::DbContainer;
use teststack::{ContainerPort, CustomContainer, stack};

#[stack(container(rabbit()))]
#[tokio::test]
async fn test(rabbit: RabbitConnection) {
    rabbit
        .create_channel()
        .await
        .expect("failed to create channel");
}

fn rabbit() -> ContainerRequest<RabbitMq> {
    RabbitMq::default().with_tag("3.11.0-alpine")
}

struct RabbitConnection(lapin::Connection);

impl std::ops::Deref for RabbitConnection {
    type Target = lapin::Connection;
    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

impl teststack::Init<RabbitConnection> for CustomContainer {
    async fn init(self) -> RabbitConnection {
        let port = self
            .get_host_port_ipv4(ContainerPort::Tcp(5672))
            .await
            .unwrap();

        let url = format!("amqp://guest:guest@localhost:{port}");
        let conn = lapin::Connection::connect(&url, lapin::ConnectionProperties::default())
            .await
            .unwrap();
        RabbitConnection(conn)
    }
}
```

If you don't need any custom configuration, you can simplify the container setup by returning the image directly:

```rust
use testcontainers_modules::rabbitmq::RabbitMq;
use teststack::{CustomContainer, stack};

#[stack(container(RabbitMq::default()))]
#[tokio::test]
async fn test(rabbit: CustomContainer) {
    // test
}
```

# Design Overview

All containers are shared per image type.
Cleanup is performed once, when the test process exits or receives a Ctrl+C signal.


# Cleanup Strategy

Test containers are gracefully cleaned up on process exit using ctor and dtor, with support for both async and blocking cleanup.

# Features

* `postgres` – enable PostgreSQL test container support
* `mysql` – enable MySQL test container support