hexcfg 1.1.4

A hexagonal architecture configuration loading crate with multi-source support
Documentation
# Docker-Based Integration Testing

This document explains the Docker-based integration testing setup for the Redis and etcd adapters.

## Overview

The configuration crate includes integration tests that:
- ✅ Use real Redis and etcd containers via Docker
- ✅ Automatically detect Docker availability
- ✅ Skip gracefully with warnings if Docker is not available
- ✅ Clean up containers automatically after tests
- ✅ Work with standard `cargo test` - no special tools required

## Implementation

### Architecture

```
tests/
├── docker_helpers.rs          # Docker detection utilities
├── redis_integration_tests.rs # Redis adapter tests
└── etcd_integration_tests.rs  # etcd adapter tests
```

### Key Components

#### 1. Docker Detection (`docker_helpers.rs`)

```rust
pub fn is_docker_available() -> bool {
    // Checks once, caches result
    // Uses `docker ps` command
}

pub fn print_docker_unavailable_warning(test_name: &str) {
    // Prints helpful warning message
}
```

#### 2. Test Structure

Each test follows this pattern:

```rust
#[tokio::test]
async fn test_something() {
    // 1. Check Docker availability
    if !docker_helpers::is_docker_available() {
        docker_helpers::print_docker_unavailable_warning("test name");
        return;  // Skip test, don't fail
    }

    // 2. Start container with testcontainers
    let docker = clients::Cli::default();
    let container = docker.run(Redis::default());

    // 3. Run test
    // ...

    // 4. Container auto-cleanup when dropped
}
```

## Running Tests

### Without Docker

```bash
$ cargo test
...
⚠️  SKIPPED: Redis integration test - Docker is not available
   To run this test, ensure Docker is installed and running.
   Installation: https://docs.docker.com/get-docker/

test result: ok. 125 passed; 0 failed; 0 ignored; 0 measured
```

Tests pass, but Docker tests are skipped with clear warnings.

### With Docker

```bash
# Terminal 1: Start Docker daemon
$ sudo systemctl start docker

# Terminal 2: Run tests
$ cargo test --features redis
...
test redis_tests::test_redis_hash_mode_get ... ok
test redis_tests::test_redis_string_keys_mode_get ... ok
test redis_tests::test_redis_reload ... ok
...
test result: ok. 133 passed; 0 failed; 0 ignored
```

All tests run including Docker-based ones.

## Benefits of This Approach

### 1. Zero Special Tooling

- ✅ Standard `cargo test` - no custom commands
- ✅ Works in any Rust environment
- ✅ No build scripts or custom test runners
- ✅ IDE test integration works out of the box

### 2. Flexible Execution

- ✅ Developers without Docker can run most tests
- ✅ CI can run basic tests without Docker setup
- ✅ Full tests when Docker is available
- ✅ No test failures due to missing Docker

### 3. Clear Feedback

```
⚠️  SKIPPED: etcd integration test - Docker is not available
   To run this test, ensure Docker is installed and running.
   Installation: https://docs.docker.com/get-docker/
```

Users immediately know:
- Why the test was skipped
- What they need to do to enable it
- Where to get Docker

## Alternative: cargo-xtask Pattern

While not needed for this use case, `cargo xtask` is an alternative for complex test scenarios:

### When to Use cargo-xtask

- Complex multi-step test workflows
- Need to compile test infrastructure
- Custom test runners with special logic
- Orchestrating multiple services

### Example Structure

```
project/
├── Cargo.toml
├── xtask/
│   ├── Cargo.toml
│   └── src/
│       └── main.rs  # Custom test commands
└── src/
    └── lib.rs
```

### Example xtask

```rust
// xtask/src/main.rs
use std::process::Command;

fn main() {
    let task = std::env::args().nth(1);
    match task.as_deref() {
        Some("test-docker") => {
            // Start Docker containers
            // Run tests
            // Clean up
        }
        Some("test-all") => {
            // Run all test suites
        }
        _ => {
            println!("Available tasks:");
            println!("  cargo xtask test-docker");
            println!("  cargo xtask test-all");
        }
    }
}
```

### Usage

```bash
cargo xtask test-docker
```

### Why We Didn't Use xtask

For this project, `testcontainers` with feature detection is simpler:

| Aspect | testcontainers | cargo-xtask |
|--------|----------------|-------------|
| Setup | Add dev-dependency | Create xtask workspace |
| Code | Tests look normal | Custom runner code |
| IDE | Works automatically | Requires configuration |
| CI | Standard cargo test | Custom commands |
| Learning curve | Low | Medium |
| Flexibility | High enough | Very high |

## Comparison: testcontainers vs Manual Docker

### testcontainers (Our Approach)

```rust
let docker = clients::Cli::default();
let container = docker.run(Redis::default());
let port = container.get_host_port_ipv4(6379);
// Container auto-cleanup
```

**Pros:**
- Automatic container lifecycle
- Type-safe container configuration
- Guaranteed cleanup (RAII)
- Port conflict handling
- Works on all platforms

**Cons:**
- Requires testcontainers crate
- Some overhead

### Manual Docker (Alternative)

```rust
std::process::Command::new("docker")
    .args(["run", "-d", "-p", "6379:6379", "redis"])
    .output()?;

// ... test ...

std::process::Command::new("docker")
    .args(["stop", container_id])
    .output()?;
```

**Pros:**
- No extra dependencies
- Full control

**Cons:**
- Manual cleanup required
- Error-prone
- Port conflict management needed
- Cleanup on panic/failure is hard
- Platform-specific issues

## CI/CD Integration

### GitHub Actions Example

```yaml
name: Tests

on: [push, pull_request]

jobs:
  # Fast tests without Docker
  test-basic:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions-rs/toolchain@v1
      - run: cargo test

  # Full tests with Docker
  test-docker:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions-rs/toolchain@v1

      # Docker is pre-installed on GitHub Actions runners

      - name: Install protoc (for etcd)
        run: sudo apt-get install -y protobuf-compiler

      - name: Run Redis tests
        run: cargo test --features redis

      - name: Run etcd tests
        run: cargo test --features etcd
```

### GitLab CI Example

```yaml
test:basic:
  script:
    - cargo test

test:docker:
  image: rust:latest
  services:
    - docker:dind
  variables:
    DOCKER_HOST: tcp://docker:2375
  before_script:
    - apt-get update && apt-get install -y protobuf-compiler
  script:
    - cargo test --features redis
    - cargo test --features etcd
```

## Testing the Tests

To verify Docker detection works:

```bash
# Test with Docker available
cargo test --features redis -- --nocapture

# Test without Docker (stop Docker first)
sudo systemctl stop docker
cargo test --features redis -- --nocapture
# Should see skip warnings

# Restart Docker
sudo systemctl start docker
```

## Troubleshooting

### Tests hang

**Cause**: Docker daemon not responding.

**Solution**:
```bash
# Restart Docker
sudo systemctl restart docker

# Check status
docker ps
```

### Container port conflicts

**Cause**: Port already in use.

**testcontainers handles this automatically** by using random host ports. You don't need to worry about conflicts.

### Permission denied

**Cause**: User not in docker group.

**Solution**:
```bash
sudo usermod -aG docker $USER
newgrp docker
```

## Best Practices

### 1. Keep Tests Isolated

Each test starts fresh containers - no shared state.

### 2. Use Reasonable Timeouts

```rust
// Give containers time to start
tokio::time::sleep(Duration::from_millis(500)).await;
```

### 3. Test Real Scenarios

Use containers to test:
- Actual network communication
- Real serialization
- Error handling (network failures, etc.)

### 4. Don't Test Container Startup

Test your code, not testcontainers:

```rust
// ❌ Don't test if container starts
assert!(container_started);

// ✅ Test your adapter works
assert_eq!(adapter.get(&key)?, expected_value);
```

## Summary

This setup provides:
- ✅ Real integration testing with Docker
- ✅ Graceful fallback without Docker
- ✅ Standard tooling (cargo test)
- ✅ Clear user feedback
- ✅ Easy CI/CD integration
- ✅ Low maintenance overhead

The key insight: **Use feature detection instead of mandatory Docker**. This makes tests more accessible while still providing thorough testing when possible.