clawspec_core/_tutorial/chapter_7.rs
1//! # Chapter 7: Test Integration
2//!
3//! This chapter covers using [`TestClient`][crate::test_client::TestClient] for
4//! end-to-end testing with automatic server lifecycle management.
5//!
6//! ## Why TestClient?
7//!
8//! While [`ApiClient`][crate::ApiClient] works against any HTTP server,
9//! [`TestClient`][crate::test_client::TestClient] provides:
10//!
11//! - **Automatic server startup** on a random port
12//! - **Health checking** with exponential backoff
13//! - **Automatic cleanup** when tests complete
14//! - **Direct access** to all `ApiClient` methods
15//!
16//! ## Implementing TestServer
17//!
18//! First, implement the [`TestServer`][crate::test_client::TestServer] trait for your server:
19//!
20//! ```rust,no_run
21//! use clawspec_core::test_client::{TestServer, TestServerConfig, HealthStatus};
22//! use clawspec_core::ApiClient;
23//! use std::net::TcpListener;
24//!
25//! #[derive(Debug)]
26//! struct MyAppServer;
27//!
28//! impl TestServer for MyAppServer {
29//! type Error = std::io::Error;
30//!
31//! async fn launch(&self, listener: TcpListener) -> Result<(), Self::Error> {
32//! // Convert to async listener
33//! listener.set_nonblocking(true)?;
34//! let listener = tokio::net::TcpListener::from_std(listener)?;
35//!
36//! // Start your server (Axum, Actix, Warp, etc.)
37//! // my_app::run(listener).await?;
38//!
39//! Ok(())
40//! }
41//! }
42//! ```
43//!
44//! ## Custom Health Checks
45//!
46//! Override `is_healthy` for custom health checking:
47//!
48//! ```rust,no_run
49//! use clawspec_core::test_client::{TestServer, HealthStatus};
50//! use clawspec_core::ApiClient;
51//! use std::net::TcpListener;
52//!
53//! # #[derive(Debug)]
54//! # struct MyAppServer;
55//! impl TestServer for MyAppServer {
56//! type Error = std::io::Error;
57//!
58//! async fn launch(&self, listener: TcpListener) -> Result<(), Self::Error> {
59//! listener.set_nonblocking(true)?;
60//! let _ = tokio::net::TcpListener::from_std(listener)?;
61//! Ok(())
62//! }
63//!
64//! async fn is_healthy(&self, client: &mut ApiClient) -> Result<HealthStatus, Self::Error> {
65//! // Check your actual health endpoint
66//! match client.get("/health")
67//! .expect("valid path")
68//! .without_collection() // Don't include in OpenAPI
69//! .await
70//! {
71//! Ok(_) => Ok(HealthStatus::Healthy),
72//! Err(_) => Ok(HealthStatus::Unhealthy),
73//! }
74//! }
75//! }
76//! ```
77//!
78//! ## Configuring the Test Server
79//!
80//! Use [`TestServerConfig`][crate::test_client::TestServerConfig] for customization:
81//!
82//! ```rust,no_run
83//! use clawspec_core::test_client::{TestServer, TestServerConfig};
84//! use clawspec_core::ApiClient;
85//! use utoipa::openapi::{InfoBuilder, ServerBuilder};
86//! use std::net::TcpListener;
87//! use std::time::Duration;
88//!
89//! # #[derive(Debug)]
90//! # struct MyAppServer;
91//! impl TestServer for MyAppServer {
92//! type Error = std::io::Error;
93//!
94//! async fn launch(&self, listener: TcpListener) -> Result<(), Self::Error> {
95//! listener.set_nonblocking(true)?;
96//! let _ = tokio::net::TcpListener::from_std(listener)?;
97//! Ok(())
98//! }
99//!
100//! fn config(&self) -> TestServerConfig {
101//! // Configure the API client with metadata
102//! let client_builder = ApiClient::builder()
103//! .with_base_path("/api/v1").expect("valid path")
104//! .with_info(
105//! InfoBuilder::new()
106//! .title("My API")
107//! .version("1.0.0")
108//! .build()
109//! )
110//! .add_server(
111//! ServerBuilder::new()
112//! .url("https://api.example.com")
113//! .description(Some("Production"))
114//! .build()
115//! );
116//!
117//! TestServerConfig {
118//! api_client: Some(client_builder),
119//! min_backoff_delay: Duration::from_millis(10),
120//! max_backoff_delay: Duration::from_secs(5),
121//! backoff_jitter: true,
122//! max_retry_attempts: 20,
123//! }
124//! }
125//! }
126//! ```
127//!
128//! ## Writing Tests
129//!
130//! Use [`TestClient::start`][crate::test_client::TestClient::start] in your tests:
131//!
132//! ```rust,no_run
133//! use clawspec_core::test_client::TestClient;
134//! # use clawspec_core::test_client::TestServer;
135//! # use std::net::TcpListener;
136//! # #[derive(Debug)]
137//! # struct MyAppServer;
138//! # impl TestServer for MyAppServer {
139//! # type Error = std::io::Error;
140//! # async fn launch(&self, listener: TcpListener) -> Result<(), Self::Error> {
141//! # listener.set_nonblocking(true)?;
142//! # let _ = tokio::net::TcpListener::from_std(listener)?;
143//! # Ok(())
144//! # }
145//! # }
146//! use serde::{Deserialize, Serialize};
147//! use utoipa::ToSchema;
148//!
149//! #[derive(Serialize, ToSchema)]
150//! struct CreateUser { name: String }
151//!
152//! #[derive(Deserialize, ToSchema)]
153//! struct User { id: u64, name: String }
154//!
155//! #[tokio::test]
156//! async fn test_user_crud() -> Result<(), Box<dyn std::error::Error>> {
157//! // Start server and get client
158//! let mut client = TestClient::start(MyAppServer).await?;
159//!
160//! // Create user
161//! let user: User = client.post("/users")?
162//! .json(&CreateUser { name: "Alice".to_string() })?
163//! .with_tag("users")
164//! .await?
165//! .as_json()
166//! .await?;
167//!
168//! assert_eq!(user.name, "Alice");
169//!
170//! // Get user
171//! let fetched: User = client.get(format!("/users/{}", user.id))?
172//! .with_tag("users")
173//! .await?
174//! .as_json()
175//! .await?;
176//!
177//! assert_eq!(fetched.id, user.id);
178//!
179//! Ok(())
180//! } // Server automatically stops when client is dropped
181//! ```
182//!
183//! ## Generating OpenAPI
184//!
185//! Use [`write_openapi`][crate::test_client::TestClient::write_openapi] to save the spec:
186//!
187//! ```rust,no_run
188//! use clawspec_core::test_client::TestClient;
189//! # use clawspec_core::test_client::TestServer;
190//! # use std::net::TcpListener;
191//! # #[derive(Debug)]
192//! # struct MyAppServer;
193//! # impl TestServer for MyAppServer {
194//! # type Error = std::io::Error;
195//! # async fn launch(&self, listener: TcpListener) -> Result<(), Self::Error> {
196//! # listener.set_nonblocking(true)?;
197//! # let _ = tokio::net::TcpListener::from_std(listener)?;
198//! # Ok(())
199//! # }
200//! # }
201//!
202//! #[tokio::test]
203//! async fn generate_openapi() -> Result<(), Box<dyn std::error::Error>> {
204//! let mut client = TestClient::start(MyAppServer).await?;
205//!
206//! // Exercise all your API endpoints...
207//! client.get("/users")?.with_tag("users").await?;
208//! client.post("/users")?.with_tag("users").await?;
209//! client.get("/users/1")?.with_tag("users").await?;
210//! client.delete("/users/1")?.with_tag("users").await?;
211//!
212//! // Generate OpenAPI spec (format detected by extension)
213//! client.write_openapi("docs/openapi.yml").await?;
214//! // Or JSON: client.write_openapi("docs/openapi.json").await?;
215//!
216//! Ok(())
217//! }
218//! ```
219//!
220//! ## Test Organization Pattern
221//!
222//! A common pattern is to have a dedicated test for OpenAPI generation:
223//!
224//! ```rust,no_run
225//! // tests/generate_openapi.rs
226//! use clawspec_core::test_client::TestClient;
227//! # use clawspec_core::test_client::TestServer;
228//! # use std::net::TcpListener;
229//! # #[derive(Debug)]
230//! # struct MyAppServer;
231//! # impl TestServer for MyAppServer {
232//! # type Error = std::io::Error;
233//! # async fn launch(&self, listener: TcpListener) -> Result<(), Self::Error> {
234//! # listener.set_nonblocking(true)?;
235//! # let _ = tokio::net::TcpListener::from_std(listener)?;
236//! # Ok(())
237//! # }
238//! # }
239//!
240//! #[tokio::test]
241//! async fn generate_openapi() -> Result<(), Box<dyn std::error::Error>> {
242//! let mut client = TestClient::start(MyAppServer).await?;
243//!
244//! // Call helper functions that exercise different parts of the API
245//! test_users_endpoints(&mut client).await?;
246//! test_posts_endpoints(&mut client).await?;
247//! test_error_cases(&mut client).await?;
248//!
249//! // Generate the spec
250//! client.write_openapi("docs/openapi.yml").await?;
251//!
252//! Ok(())
253//! }
254//!
255//! async fn test_users_endpoints(client: &mut TestClient<MyAppServer>) -> Result<(), Box<dyn std::error::Error>> {
256//! client.get("/users")?
257//! .with_tag("users")
258//! .with_description("List all users")
259//! .await?;
260//! // ... more user endpoints
261//! Ok(())
262//! }
263//! # async fn test_posts_endpoints(client: &mut TestClient<MyAppServer>) -> Result<(), Box<dyn std::error::Error>> { Ok(()) }
264//! # async fn test_error_cases(client: &mut TestClient<MyAppServer>) -> Result<(), Box<dyn std::error::Error>> { Ok(()) }
265//! ```
266//!
267//! ## Accessing the Underlying Client
268//!
269//! `TestClient` derefs to `ApiClient`, so all methods are available:
270//!
271//! ```rust,no_run
272//! use clawspec_core::test_client::TestClient;
273//! # use clawspec_core::test_client::TestServer;
274//! # use std::net::TcpListener;
275//! # #[derive(Debug)]
276//! # struct MyAppServer;
277//! # impl TestServer for MyAppServer {
278//! # type Error = std::io::Error;
279//! # async fn launch(&self, listener: TcpListener) -> Result<(), Self::Error> {
280//! # listener.set_nonblocking(true)?;
281//! # let _ = tokio::net::TcpListener::from_std(listener)?;
282//! # Ok(())
283//! # }
284//! # }
285//! # use serde::{Serialize, Deserialize};
286//! # use utoipa::ToSchema;
287//! # #[derive(Serialize, Deserialize, ToSchema)]
288//! # struct MySchema { field: String }
289//!
290//! # #[tokio::main]
291//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
292//! let mut client = TestClient::start(MyAppServer).await?;
293//!
294//! // All ApiClient methods work directly
295//! client.register_schema::<MySchema>().await;
296//! let spec = client.collected_openapi().await;
297//! # Ok(())
298//! # }
299//! ```
300//!
301//! ## Key Points
302//!
303//! - Implement [`TestServer`][crate::test_client::TestServer] for your web framework
304//! - Override `is_healthy()` for custom health checking
305//! - Override `config()` for API metadata and timing settings
306//! - Use `TestClient::start()` in tests for automatic lifecycle management
307//! - Use `write_openapi()` to generate specs in YAML or JSON format
308//! - Server stops automatically when `TestClient` is dropped
309//!
310//! ## Complete Example
311//!
312//! For a full working example with Axum, see the
313//! [axum-example](https://github.com/ilaborie/clawspec/tree/main/examples/axum-example)
314//! in the Clawspec repository.
315//!
316//! ---
317//!
318//! Congratulations! You've completed the Clawspec tutorial. You now know how to:
319//!
320//! - Create and configure API clients
321//! - Make requests with various parameters
322//! - Handle different response types
323//! - Customize OpenAPI output
324//! - Use redaction for stable examples
325//! - Integrate with test frameworks
326//!
327//! For more details, explore the [API documentation][crate] or check out the
328//! [GitHub repository](https://github.com/ilaborie/clawspec).