From aef738dce6589a38cf06d4f1c80f7812076e8f22 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 7 Feb 2026 03:49:20 +0000 Subject: [PATCH 1/2] docs: update versions to 0.1.300 and expand testing documentation - Updated `sqlx` version in `db_integration.md` to `0.8` (fixing feature flags). - Verified `rustapi-rs` versions in docs are `0.1.300`. - Expanded `rustapi_testing.md` to include detailed documentation for `MockServer`, `TestClient`, and Expectations. - Updated Learning Path (`docs/cookbook/src/learning/README.md`) to include "Reliable Testing" and "Service Mocking". Co-authored-by: Tuntii <121901995+Tuntii@users.noreply.github.com> --- docs/cookbook/src/crates/rustapi_testing.md | 127 +++++++++++++++++--- docs/cookbook/src/learning/README.md | 5 +- docs/cookbook/src/recipes/db_integration.md | 2 +- 3 files changed, 118 insertions(+), 16 deletions(-) diff --git a/docs/cookbook/src/crates/rustapi_testing.md b/docs/cookbook/src/crates/rustapi_testing.md index 46418dad..5a52d0b9 100644 --- a/docs/cookbook/src/crates/rustapi_testing.md +++ b/docs/cookbook/src/crates/rustapi_testing.md @@ -3,27 +3,126 @@ **Lens**: "The Auditor" **Philosophy**: "Trust, but verify." +`rustapi-testing` provides a comprehensive suite of tools for integration testing your RustAPI applications. It focuses on two main areas: +1. **In-process API testing**: Testing your endpoints without binding to a real TCP port. +2. **External service mocking**: Mocking downstream services (like payment gateways or auth providers) that your API calls. + ## The `TestClient` -Integration testing is often painful. We make it easy. `TestClient` spawns your `RustApi` application without binding to a real TCP port, communicating directly with the service layer. +Integration testing is often slow and painful because it involves spinning up a server, waiting for ports, and managing child processes. `TestClient` solves this by wrapping your `RustApi` application and executing requests directly against the service layer. + +### Basic Usage + +```rust,ignore +use rustapi_rs::prelude::*; +use rustapi_testing::TestClient; + +#[tokio::test] +async fn test_hello_world() { + let app = RustApi::new().route("/", get(|| async { "Hello!" })); + let client = TestClient::new(app); -```rust -let client = TestClient::new(app); + let response = client.get("/").await; + + response + .assert_status(200) + .assert_body_contains("Hello!"); +} ``` -## Fluent Assertions +### Testing JSON APIs + +The client provides fluent helpers for JSON APIs. + +```rust,ignore +#[derive(Serialize)] +struct CreateUser { + username: String, +} + +#[tokio::test] +async fn test_create_user() { + let app = RustApi::new().route("/users", post(create_user_handler)); + let client = TestClient::new(app); + + let response = client.post_json("/users", &CreateUser { + username: "alice".into() + }).await; + + response + .assert_status(201) + .assert_json(&serde_json::json!({ + "id": 1, + "username": "alice" + })); +} +``` + +## Mocking Services with `MockServer` + +Real-world applications usually talk to other services. `MockServer` allows you to spin up a lightweight HTTP server that responds to requests based on pre-defined expectations. + +### Setting up a Mock Server + +```rust,ignore +use rustapi_testing::{MockServer, MockResponse, RequestMatcher}; + +#[tokio::test] +async fn test_external_integration() { + // 1. Start the mock server + let server = MockServer::start().await; + + // 2. Define an expectation + server.expect(RequestMatcher::new(Method::GET, "/external-api/data")) + .respond_with(MockResponse::new() + .status(StatusCode::OK) + .json(serde_json::json!({ "result": "success" }))) + .times(1); + + // 3. Configure your app to use the mock server's URL + let app = create_app_with_config(Config { + external_api_url: server.base_url(), + }); + + let client = TestClient::new(app); + + // 4. Run your test + client.get("/my-endpoint-calling-external").await.assert_status(200); +} +``` + +### Expectations + +You can define strict expectations on how your application interacts with the mock server. + +#### Matching Requests + +`RequestMatcher` allows matching by method, path, headers, and body. + +```rust,ignore +// Match a POST request with specific body +server.expect(RequestMatcher::new(Method::POST, "/webhook") + .body_contains("event_type=payment_success")) + .respond_with(MockResponse::new().status(200)); +``` + +#### Verification + +The `MockServer` automatically verifies that all expectations were met when it is dropped (at the end of the test scope). If an expectation was set to be called `once` but was never called, the test will panic. -The client provides a fluent API for making requests and asserting responses. +- `.once()`: Must be called exactly once (default). +- `.times(n)`: Must be called exactly `n` times. +- `.at_least_once()`: Must be called 1 or more times. +- `.never()`: Must not be called. -```rust -client.post("/login") - .json(&credentials) - .send() - .await - .assert_status(200) - .assert_header("Set-Cookie", "session=..."); +```rust,ignore +// Ensure we don't call the billing API if validation fails +server.expect(RequestMatcher::new(Method::POST, "/charge")) + .never(); ``` -## Mocking Services +## Best Practices -Because `rustapi-rs` relies heavily on Dependency Injection via `State`, you can easily inject mock implementations of your database or downstream services when creating the `RustApi` instance for your test. +1. **Dependency Injection**: Design your application `State` to accept base URLs for external services so you can inject the `MockServer` URL during tests. +2. **Isolation**: Create a new `MockServer` for each test case to ensure no shared state or interference. +3. **Fluent Assertions**: Use the chainable assertion methods on `TestResponse` to keep tests readable. diff --git a/docs/cookbook/src/learning/README.md b/docs/cookbook/src/learning/README.md index a1c4f229..ff8471d6 100644 --- a/docs/cookbook/src/learning/README.md +++ b/docs/cookbook/src/learning/README.md @@ -66,7 +66,8 @@ Design and build distributed systems with RustAPI. | 3 | `rate-limit-demo` | API protection, throttling | | 4 | `microservices` | Service communication patterns | | 5 | `microservices-advanced` | Service discovery, Consul integration | -| 6 | Background jobs (conceptual) | Background processing with `rustapi-jobs`, Redis/Postgres backends | +| 6 | Service Mocking | Testing microservices with `MockServer` from `rustapi-testing` | +| 7 | Background jobs (conceptual) | Background processing with `rustapi-jobs`, Redis/Postgres backends | > Note: The **Background jobs (conceptual)** step refers to using the `rustapi-jobs` crate rather than a standalone example project. **Related Cookbook Recipes:** @@ -122,8 +123,10 @@ Build robust, observable, and secure systems. | 4 | **Optimization** | Configure [Caching and Deduplication](../crates/rustapi_extras.md#optimization) | | 5 | **Background Jobs** | Implement [Reliable Job Queues](../crates/rustapi_jobs.md) | | 6 | **Debugging** | Set up [Time-Travel Debugging](../recipes/replay.md) | +| 7 | **Reliable Testing** | Master [Mocking and Integration Testing](../crates/rustapi_testing.md) | **Related Cookbook Recipes:** +- [rustapi-testing: The Auditor](../crates/rustapi_testing.md) - [rustapi-extras: The Toolbox](../crates/rustapi_extras.md) - [Time-Travel Debugging](../recipes/replay.md) - [rustapi-jobs: The Workhorse](../crates/rustapi_jobs.md) diff --git a/docs/cookbook/src/recipes/db_integration.md b/docs/cookbook/src/recipes/db_integration.md index 4b3d6fcf..804829ff 100644 --- a/docs/cookbook/src/recipes/db_integration.md +++ b/docs/cookbook/src/recipes/db_integration.md @@ -8,7 +8,7 @@ This recipe shows how to integrate PostgreSQL/MySQL/SQLite using a global connec ```toml [dependencies] -sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "postgres", "uuid"] } +sqlx = { version = "0.8", features = ["runtime-tokio", "tls-rustls", "postgres", "uuid"] } serde = { version = "1", features = ["derive"] } tokio = { version = "1", features = ["full"] } dotenvy = "0.15" From 6f8553a4ae4d435169d7fa19f58a85f75b91bdee Mon Sep 17 00:00:00 2001 From: Tunay <121901995+Tuntii@users.noreply.github.com> Date: Sat, 7 Feb 2026 07:24:09 +0300 Subject: [PATCH 2/2] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- docs/cookbook/src/crates/rustapi_testing.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/cookbook/src/crates/rustapi_testing.md b/docs/cookbook/src/crates/rustapi_testing.md index 5a52d0b9..b500dbd2 100644 --- a/docs/cookbook/src/crates/rustapi_testing.md +++ b/docs/cookbook/src/crates/rustapi_testing.md @@ -102,8 +102,8 @@ You can define strict expectations on how your application interacts with the mo ```rust,ignore // Match a POST request with specific body server.expect(RequestMatcher::new(Method::POST, "/webhook") - .body_contains("event_type=payment_success")) - .respond_with(MockResponse::new().status(200)); + .body_string("event_type=payment_success".into())) + .respond_with(MockResponse::new().status(StatusCode::OK)); ``` #### Verification