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).