Skip to content

Commit fff81f4

Browse files
committed
docs: add testing_utilities example demonstrating ContainerGuard usage
Comprehensive example showing: - Basic ContainerGuard usage with automatic cleanup - Connection strings for service access - Configuration options (stop timeout, debug mode, reuse) - ContainerGuardSet for multi-container tests - Container info access (logs, IDs) - Test fixture patterns All tests use unique ports for parallel safety.
1 parent 277967a commit fff81f4

1 file changed

Lines changed: 373 additions & 0 deletions

File tree

examples/testing_utilities.rs

Lines changed: 373 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,373 @@
1+
//! # Testing Utilities Example
2+
//!
3+
//! This example demonstrates how to use docker-wrapper's testing utilities
4+
//! for integration testing with containers. These utilities provide RAII-style
5+
//! lifecycle management, ensuring containers are automatically cleaned up.
6+
//!
7+
//! ## Features Demonstrated
8+
//!
9+
//! - `ContainerGuard`: Single container lifecycle management
10+
//! - `ContainerGuardSet`: Multi-container test environments
11+
//! - Automatic cleanup on drop (even on panic)
12+
//! - Ready checks and connection strings
13+
//! - Debug mode for failed tests
14+
//!
15+
//! ## Running the Examples
16+
//!
17+
//! Run specific tests:
18+
//! ```bash
19+
//! cargo test --example testing_utilities --features testing
20+
//! ```
21+
//!
22+
//! Run with output:
23+
//! ```bash
24+
//! cargo test --example testing_utilities --features testing -- --nocapture
25+
//! ```
26+
27+
#[cfg(test)]
28+
use docker_wrapper::testing::{ContainerGuard, ContainerGuardSet};
29+
#[cfg(test)]
30+
use docker_wrapper::{RedisTemplate, Template};
31+
#[cfg(test)]
32+
use std::sync::atomic::{AtomicU16, Ordering};
33+
#[cfg(test)]
34+
use std::time::Duration;
35+
36+
/// Generate unique container names for parallel test safety.
37+
#[cfg(test)]
38+
fn unique_name(prefix: &str) -> String {
39+
format!("{}-{}", prefix, uuid::Uuid::new_v4().simple())
40+
}
41+
42+
/// Generate unique ports for parallel test safety.
43+
/// Starts at 40000 and increments for each call.
44+
#[cfg(test)]
45+
fn unique_port() -> u16 {
46+
static PORT: AtomicU16 = AtomicU16::new(40000);
47+
PORT.fetch_add(1, Ordering::SeqCst)
48+
}
49+
50+
// ============================================================================
51+
// Basic Usage
52+
// ============================================================================
53+
54+
/// Basic container guard usage with automatic cleanup.
55+
///
56+
/// The container is automatically stopped and removed when the guard
57+
/// goes out of scope, even if the test panics.
58+
#[tokio::test]
59+
async fn test_basic_container_guard() {
60+
let name = unique_name("basic-redis");
61+
let port = unique_port();
62+
63+
// Create and start a Redis container with automatic lifecycle management
64+
let guard = ContainerGuard::new(RedisTemplate::new(&name).port(port))
65+
.wait_for_ready(true) // Wait for Redis to accept connections
66+
.start()
67+
.await
68+
.expect("Failed to start Redis container");
69+
70+
// Container is ready - verify it's running
71+
assert!(
72+
guard.is_running().await.expect("Failed to check status"),
73+
"Container should be running"
74+
);
75+
76+
// Access the underlying template if needed
77+
let template = guard.template();
78+
assert_eq!(template.config().name, name);
79+
80+
// Container is automatically stopped and removed here
81+
}
82+
83+
/// Using connection strings for easy service access.
84+
#[tokio::test]
85+
async fn test_connection_string() {
86+
let name = unique_name("conn-redis");
87+
let port = unique_port();
88+
89+
let guard = ContainerGuard::new(RedisTemplate::new(&name).port(port))
90+
.wait_for_ready(true)
91+
.start()
92+
.await
93+
.expect("Failed to start Redis");
94+
95+
// Get the connection string directly from the guard
96+
let conn = guard.connection_string();
97+
assert!(
98+
conn.starts_with("redis://"),
99+
"Should be a Redis connection URL"
100+
);
101+
assert!(
102+
conn.contains(&port.to_string()),
103+
"Should include the configured port"
104+
);
105+
106+
println!("Redis available at: {}", conn);
107+
}
108+
109+
// ============================================================================
110+
// Configuration Options
111+
// ============================================================================
112+
113+
/// Fast cleanup with custom stop timeout.
114+
///
115+
/// By default Docker waits 10 seconds for graceful shutdown. Use a shorter
116+
/// timeout for faster test cleanup when you don't need graceful shutdown.
117+
#[tokio::test]
118+
async fn test_fast_cleanup() {
119+
let name = unique_name("fast-redis");
120+
let port = unique_port();
121+
122+
let guard = ContainerGuard::new(RedisTemplate::new(&name).port(port))
123+
.wait_for_ready(true)
124+
.stop_timeout(Duration::from_secs(1)) // Fast 1-second timeout
125+
.start()
126+
.await
127+
.expect("Failed to start Redis");
128+
129+
assert!(guard.is_running().await.unwrap());
130+
131+
// Cleanup will be fast due to short timeout
132+
}
133+
134+
/// Keep containers running for debugging failed tests.
135+
///
136+
/// When `keep_on_panic(true)` is set, the container won't be removed if
137+
/// the test panics, allowing you to inspect its state for debugging.
138+
#[tokio::test]
139+
async fn test_debug_mode() {
140+
let name = unique_name("debug-redis");
141+
let port = unique_port();
142+
143+
let guard = ContainerGuard::new(RedisTemplate::new(&name).port(port))
144+
.keep_on_panic(true) // Keep container if test panics
145+
.capture_logs(true) // Print logs on panic
146+
.wait_for_ready(true)
147+
.start()
148+
.await
149+
.expect("Failed to start Redis");
150+
151+
// If this test panicked, the container would stay running
152+
// and logs would be printed to stderr for debugging
153+
assert!(guard.is_running().await.unwrap());
154+
155+
// Normal exit - container is cleaned up
156+
}
157+
158+
/// Container reuse for faster local development.
159+
///
160+
/// With `reuse_if_running(true)`, if a container with the same name is
161+
/// already running, it will be reused instead of starting a new one.
162+
#[tokio::test]
163+
async fn test_container_reuse() {
164+
// Use a fixed name and port for reuse demonstration
165+
let name = unique_name("reuse-redis");
166+
let port = unique_port();
167+
168+
// First run - starts a new container
169+
let guard1 = ContainerGuard::new(RedisTemplate::new(&name).port(port))
170+
.reuse_if_running(true)
171+
.stop_on_drop(false) // Don't stop so next test can reuse
172+
.remove_on_drop(false) // Don't remove so next test can reuse
173+
.wait_for_ready(true)
174+
.start()
175+
.await
176+
.expect("Failed to start Redis");
177+
178+
let was_reused_first = guard1.was_reused();
179+
println!("First guard - was_reused: {}", was_reused_first);
180+
181+
// Second guard - should reuse the running container
182+
let guard2 = ContainerGuard::new(RedisTemplate::new(&name).port(port))
183+
.reuse_if_running(true)
184+
.wait_for_ready(true)
185+
.start()
186+
.await
187+
.expect("Failed to start/reuse Redis");
188+
189+
// This time it should be reused (if container was still running)
190+
println!("Second guard - was_reused: {}", guard2.was_reused());
191+
assert!(guard2.was_reused(), "Second guard should reuse container");
192+
193+
// guard2 will clean up since we didn't disable cleanup
194+
}
195+
196+
/// Manual cleanup when you need explicit control.
197+
#[tokio::test]
198+
async fn test_manual_cleanup() {
199+
let name = unique_name("manual-redis");
200+
let port = unique_port();
201+
202+
let guard = ContainerGuard::new(RedisTemplate::new(&name).port(port))
203+
.wait_for_ready(true)
204+
.start()
205+
.await
206+
.expect("Failed to start Redis");
207+
208+
// Do some work...
209+
assert!(guard.is_running().await.unwrap());
210+
211+
// Explicitly clean up before the guard goes out of scope
212+
guard.cleanup().await.expect("Failed to cleanup");
213+
214+
// Guard will not try to clean up again on drop (idempotent)
215+
}
216+
217+
// ============================================================================
218+
// Multi-Container Tests
219+
// ============================================================================
220+
221+
/// Using ContainerGuardSet for multi-container test environments.
222+
///
223+
/// This is useful when your test needs multiple services that may need
224+
/// to communicate with each other.
225+
#[tokio::test]
226+
async fn test_multi_container_set() {
227+
let network = unique_name("test-net");
228+
let redis1_name = unique_name("redis-primary");
229+
let redis2_name = unique_name("redis-replica");
230+
let port1 = unique_port();
231+
let port2 = unique_port();
232+
233+
// Create a set with multiple containers on a shared network
234+
let guards = ContainerGuardSet::new()
235+
.with_network(&network) // Containers can communicate via this network
236+
.add(RedisTemplate::new(&redis1_name).port(port1))
237+
.add(RedisTemplate::new(&redis2_name).port(port2))
238+
.keep_on_panic(true) // Keep all containers for debugging
239+
.start_all()
240+
.await
241+
.expect("Failed to start container set");
242+
243+
// Verify both containers are in the set
244+
assert!(guards.contains(&redis1_name));
245+
assert!(guards.contains(&redis2_name));
246+
assert_eq!(guards.len(), 2);
247+
248+
// Get the shared network
249+
assert_eq!(guards.network(), Some(network.as_str()));
250+
251+
// List all container names
252+
println!("Running containers:");
253+
for name in guards.names() {
254+
println!(" - {}", name);
255+
}
256+
257+
// All containers and the network are cleaned up when guards is dropped
258+
}
259+
260+
/// ContainerGuardSet with custom options.
261+
#[tokio::test]
262+
async fn test_guard_set_options() {
263+
let network = unique_name("options-net");
264+
let port = unique_port();
265+
266+
let guards = ContainerGuardSet::new()
267+
.with_network(&network)
268+
.create_network(true) // Create network if it doesn't exist (default)
269+
.remove_network_on_drop(true) // Clean up network after test (default)
270+
.wait_for_ready(true) // Wait for all containers to be ready (default)
271+
.keep_on_panic(false) // Clean up even on panic
272+
.add(RedisTemplate::new(&unique_name("opt-redis")).port(port))
273+
.start_all()
274+
.await
275+
.expect("Failed to start");
276+
277+
assert_eq!(guards.len(), 1);
278+
assert!(!guards.is_empty());
279+
}
280+
281+
// ============================================================================
282+
// Accessing Container Information
283+
// ============================================================================
284+
285+
/// Accessing container details and logs.
286+
#[tokio::test]
287+
async fn test_container_info() {
288+
let name = unique_name("info-redis");
289+
let port = unique_port();
290+
291+
let guard = ContainerGuard::new(RedisTemplate::new(&name).port(port))
292+
.wait_for_ready(true)
293+
.start()
294+
.await
295+
.expect("Failed to start Redis");
296+
297+
// Get container ID (if available)
298+
if let Some(id) = guard.container_id() {
299+
println!("Container ID: {}", id);
300+
assert!(!id.is_empty());
301+
}
302+
303+
// Get connection string (includes the port)
304+
let conn = guard.connection_string();
305+
println!("Connection string: {}", conn);
306+
assert!(conn.contains(&port.to_string()));
307+
308+
// Get container logs
309+
let logs = guard.logs().await.expect("Failed to get logs");
310+
println!("Container logs:\n{}", logs);
311+
assert!(logs.contains("Ready to accept connections"));
312+
313+
// Check running status
314+
assert!(guard.is_running().await.expect("Failed to check status"));
315+
}
316+
317+
// ============================================================================
318+
// Test Fixtures Pattern
319+
// ============================================================================
320+
321+
/// Create reusable test fixtures for common setups.
322+
#[cfg(test)]
323+
async fn redis_fixture(name: &str, port: u16) -> ContainerGuard<RedisTemplate> {
324+
ContainerGuard::new(RedisTemplate::new(name).port(port))
325+
.wait_for_ready(true)
326+
.keep_on_panic(true)
327+
.capture_logs(true)
328+
.stop_timeout(Duration::from_secs(2))
329+
.start()
330+
.await
331+
.expect("Failed to create Redis fixture")
332+
}
333+
334+
/// Using the test fixture pattern.
335+
#[tokio::test]
336+
async fn test_with_fixture() {
337+
let name = unique_name("fixture-redis");
338+
let port = unique_port();
339+
let redis = redis_fixture(&name, port).await;
340+
341+
// Fixture is ready to use
342+
let conn = redis.connection_string();
343+
println!("Fixture ready at: {}", conn);
344+
345+
// Clean up is automatic
346+
}
347+
348+
// ============================================================================
349+
// Main
350+
// ============================================================================
351+
352+
fn main() {
353+
println!("Testing Utilities Example");
354+
println!("=========================");
355+
println!();
356+
println!("This example demonstrates docker-wrapper's testing utilities");
357+
println!("for integration testing with automatic container lifecycle management.");
358+
println!();
359+
println!("Run the tests with:");
360+
println!(" cargo test --example testing_utilities --features testing");
361+
println!();
362+
println!("Run with output:");
363+
println!(" cargo test --example testing_utilities --features testing -- --nocapture");
364+
println!();
365+
println!("Features demonstrated:");
366+
println!(" - ContainerGuard: Single container RAII management");
367+
println!(" - ContainerGuardSet: Multi-container test environments");
368+
println!(" - Automatic cleanup on drop and panic");
369+
println!(" - Ready checks and connection strings");
370+
println!(" - Debug mode for failed tests");
371+
println!(" - Container reuse for faster development");
372+
println!(" - Test fixture patterns");
373+
}

0 commit comments

Comments
 (0)