Expand description
§Chapter 7: Test Integration
This chapter covers using TestClient for
end-to-end testing with automatic server lifecycle management.
§Why TestClient?
While ApiClient works against any HTTP server,
TestClient provides:
- Automatic server startup on a random port
- Health checking with exponential backoff
- Automatic cleanup when tests complete
- Direct access to all
ApiClientmethods
§Implementing TestServer
First, implement the TestServer trait for your server:
use clawspec_core::test_client::{TestServer, TestServerConfig, HealthStatus};
use clawspec_core::ApiClient;
use std::net::TcpListener;
#[derive(Debug)]
struct MyAppServer;
impl TestServer for MyAppServer {
type Error = std::io::Error;
async fn launch(&self, listener: TcpListener) -> Result<(), Self::Error> {
// Convert to async listener
listener.set_nonblocking(true)?;
let listener = tokio::net::TcpListener::from_std(listener)?;
// Start your server (Axum, Actix, Warp, etc.)
// my_app::run(listener).await?;
Ok(())
}
}§Custom Health Checks
Override is_healthy for custom health checking:
use clawspec_core::test_client::{TestServer, HealthStatus};
use clawspec_core::ApiClient;
use std::net::TcpListener;
impl TestServer for MyAppServer {
type Error = std::io::Error;
async fn launch(&self, listener: TcpListener) -> Result<(), Self::Error> {
listener.set_nonblocking(true)?;
let _ = tokio::net::TcpListener::from_std(listener)?;
Ok(())
}
async fn is_healthy(&self, client: &mut ApiClient) -> Result<HealthStatus, Self::Error> {
// Check your actual health endpoint
match client.get("/health")
.expect("valid path")
.without_collection() // Don't include in OpenAPI
.await
{
Ok(_) => Ok(HealthStatus::Healthy),
Err(_) => Ok(HealthStatus::Unhealthy),
}
}
}§Configuring the Test Server
Use TestServerConfig for customization:
use clawspec_core::test_client::{TestServer, TestServerConfig};
use clawspec_core::ApiClient;
use utoipa::openapi::{InfoBuilder, ServerBuilder};
use std::net::TcpListener;
use std::time::Duration;
impl TestServer for MyAppServer {
type Error = std::io::Error;
async fn launch(&self, listener: TcpListener) -> Result<(), Self::Error> {
listener.set_nonblocking(true)?;
let _ = tokio::net::TcpListener::from_std(listener)?;
Ok(())
}
fn config(&self) -> TestServerConfig {
// Configure the API client with metadata
let client_builder = ApiClient::builder()
.with_base_path("/api/v1").expect("valid path")
.with_info(
InfoBuilder::new()
.title("My API")
.version("1.0.0")
.build()
)
.add_server(
ServerBuilder::new()
.url("https://api.example.com")
.description(Some("Production"))
.build()
);
TestServerConfig {
api_client: Some(client_builder),
min_backoff_delay: Duration::from_millis(10),
max_backoff_delay: Duration::from_secs(5),
backoff_jitter: true,
max_retry_attempts: 20,
}
}
}§Writing Tests
Use TestClient::start in your tests:
use clawspec_core::test_client::TestClient;
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
#[derive(Serialize, ToSchema)]
struct CreateUser { name: String }
#[derive(Deserialize, ToSchema)]
struct User { id: u64, name: String }
#[tokio::test]
async fn test_user_crud() -> Result<(), Box<dyn std::error::Error>> {
// Start server and get client
let mut client = TestClient::start(MyAppServer).await?;
// Create user
let user: User = client.post("/users")?
.json(&CreateUser { name: "Alice".to_string() })?
.with_tag("users")
.await?
.as_json()
.await?;
assert_eq!(user.name, "Alice");
// Get user
let fetched: User = client.get(format!("/users/{}", user.id))?
.with_tag("users")
.await?
.as_json()
.await?;
assert_eq!(fetched.id, user.id);
Ok(())
} // Server automatically stops when client is dropped§Generating OpenAPI
Use write_openapi to save the spec:
use clawspec_core::test_client::TestClient;
#[tokio::test]
async fn generate_openapi() -> Result<(), Box<dyn std::error::Error>> {
let mut client = TestClient::start(MyAppServer).await?;
// Exercise all your API endpoints...
client.get("/users")?.with_tag("users").await?;
client.post("/users")?.with_tag("users").await?;
client.get("/users/1")?.with_tag("users").await?;
client.delete("/users/1")?.with_tag("users").await?;
// Generate OpenAPI spec (format detected by extension)
client.write_openapi("docs/openapi.yml").await?;
// Or JSON: client.write_openapi("docs/openapi.json").await?;
Ok(())
}§Test Organization Pattern
A common pattern is to have a dedicated test for OpenAPI generation:
// tests/generate_openapi.rs
use clawspec_core::test_client::TestClient;
#[tokio::test]
async fn generate_openapi() -> Result<(), Box<dyn std::error::Error>> {
let mut client = TestClient::start(MyAppServer).await?;
// Call helper functions that exercise different parts of the API
test_users_endpoints(&mut client).await?;
test_posts_endpoints(&mut client).await?;
test_error_cases(&mut client).await?;
// Generate the spec
client.write_openapi("docs/openapi.yml").await?;
Ok(())
}
async fn test_users_endpoints(client: &mut TestClient<MyAppServer>) -> Result<(), Box<dyn std::error::Error>> {
client.get("/users")?
.with_tag("users")
.with_description("List all users")
.await?;
// ... more user endpoints
Ok(())
}§Accessing the Underlying Client
TestClient derefs to ApiClient, so all methods are available:
use clawspec_core::test_client::TestClient;
let mut client = TestClient::start(MyAppServer).await?;
// All ApiClient methods work directly
client.register_schema::<MySchema>().await;
let spec = client.collected_openapi().await;§Key Points
- Implement
TestServerfor your web framework - Override
is_healthy()for custom health checking - Override
config()for API metadata and timing settings - Use
TestClient::start()in tests for automatic lifecycle management - Use
write_openapi()to generate specs in YAML or JSON format - Server stops automatically when
TestClientis dropped
§Complete Example
For a full working example with Axum, see the axum-example in the Clawspec repository.
Congratulations! You’ve completed the Clawspec tutorial. You now know how to:
- Create and configure API clients
- Make requests with various parameters
- Handle different response types
- Customize OpenAPI output
- Use redaction for stable examples
- Integrate with test frameworks
For more details, explore the API documentation or check out the GitHub repository.