Skip to main content

volter_testing/
lib.rs

1//! In-process test client for Volter applications.
2//!
3//! [`TestClient`] wraps a [`Router`](volter_router::Router) and drives it
4//! directly in memory — no real TCP socket is bound, so tests are fast and
5//! don't fight over ports in CI.
6//!
7//! # Quick start
8//!
9//! ```rust
10//! use volter_testing::TestClient;
11//! use volter_router::{Router, get};
12//!
13//! async fn handler() -> &'static str { "ok" }
14//!
15//! # #[tokio::main(flavor = "current_thread")]
16//! # async fn main() {
17//! let client = TestClient::new(Router::new().route("/", get(handler)));
18//!
19//! let response = client.get("/").send().await;
20//! assert_eq!(response.status(), 200);
21//!
22//! let body = response.text().await.unwrap();
23//! assert_eq!(body, "ok");
24//! # }
25//! ```
26//!
27//! # JSON round-trip
28//!
29//! ```rust
30//! use serde::{Serialize, Deserialize};
31//! use volter_testing::TestClient;
32//! use volter_router::{Router, post};
33//! use volter_extract::Json;
34//!
35//! #[derive(Serialize, Deserialize, PartialEq, Debug)]
36//! struct Payload { name: String }
37//!
38//! async fn echo(Json(p): Json<Payload>) -> Json<Payload> { Json(p) }
39//!
40//! # #[tokio::main(flavor = "current_thread")]
41//! # async fn main() {
42//! let client = TestClient::new(Router::new().route("/echo", post(echo)));
43//!
44//! let response = client
45//!     .post("/echo")
46//!     .json(&Payload { name: "ferris".into() })
47//!     .send()
48//!     .await;
49//!
50//! let body: Payload = response.json().await.unwrap();
51//! assert_eq!(body, Payload { name: "ferris".into() });
52//! # }
53//! ```
54//!
55//! See `ARCHITECTURE.md` → "Testing architecture" for the full design.
56
57#![deny(missing_docs)]
58#![deny(
59    clippy::unwrap_used,
60    clippy::expect_used,
61    clippy::panic,
62    clippy::indexing_slicing
63)]
64
65mod request;
66mod response;
67
68pub use request::TestRequestBuilder;
69pub use response::{BodyError, TestResponse};
70
71use http::Method;
72use volter_router::Router;
73
74/// An in-process test client for Volter applications.
75///
76/// Wraps a [`Router`] and provides ergonomic helpers for issuing requests
77/// and inspecting responses without binding a real TCP socket.
78///
79/// The type parameter `S` corresponds to the router's application state.
80/// Most tests use a stateless router (`Router::new()`) which gives
81/// `TestClient::new(router)` a `TestClient<()>`.
82///
83/// Each call to `get` / `post` / `request` clones the router (a cheap
84/// `Arc` bump) so the client does not require `&mut` access.
85///
86/// # Example
87///
88/// ```rust
89/// use volter_testing::TestClient;
90/// use volter_router::{Router, get};
91///
92/// async fn handler() -> &'static str { "hello" }
93///
94/// # #[tokio::main(flavor = "current_thread")]
95/// # async fn main() {
96/// let client = TestClient::new(Router::new().route("/", get(handler)));
97/// let resp = client.get("/").send().await;
98/// assert_eq!(resp.status(), 200);
99/// # }
100/// ```
101pub struct TestClient<S = ()> {
102    router: Router<S>,
103}
104
105impl<S: Clone + Send + 'static> TestClient<S> {
106    /// Create a new `TestClient` wrapping the given router.
107    pub fn new(router: Router<S>) -> Self {
108        Self { router }
109    }
110
111    /// Start building a GET request to `path`.
112    pub fn get(&self, path: &str) -> TestRequestBuilder<S> {
113        self.request(Method::GET, path)
114    }
115
116    /// Start building a POST request to `path`.
117    pub fn post(&self, path: &str) -> TestRequestBuilder<S> {
118        self.request(Method::POST, path)
119    }
120
121    /// Start building a request with an arbitrary HTTP method.
122    pub fn request(&self, method: Method, path: &str) -> TestRequestBuilder<S> {
123        TestRequestBuilder {
124            router: self.router.clone(),
125            method,
126            path: path.to_owned(),
127            headers: http::HeaderMap::new(),
128            body: None,
129        }
130    }
131}