Skip to content

Commit 6f71402

Browse files
authored
Merge pull request #105 from Tuntii/docs/update-versions-and-testing-12707341404442644071
docs: update versions to 0.1.300 and expand testing documentation
2 parents facb91e + 6f8553a commit 6f71402

File tree

3 files changed

+118
-16
lines changed

3 files changed

+118
-16
lines changed

docs/cookbook/src/crates/rustapi_testing.md

Lines changed: 113 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,27 +3,126 @@
33
**Lens**: "The Auditor"
44
**Philosophy**: "Trust, but verify."
55

6+
`rustapi-testing` provides a comprehensive suite of tools for integration testing your RustAPI applications. It focuses on two main areas:
7+
1. **In-process API testing**: Testing your endpoints without binding to a real TCP port.
8+
2. **External service mocking**: Mocking downstream services (like payment gateways or auth providers) that your API calls.
9+
610
## The `TestClient`
711

8-
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.
12+
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.
13+
14+
### Basic Usage
15+
16+
```rust,ignore
17+
use rustapi_rs::prelude::*;
18+
use rustapi_testing::TestClient;
19+
20+
#[tokio::test]
21+
async fn test_hello_world() {
22+
let app = RustApi::new().route("/", get(|| async { "Hello!" }));
23+
let client = TestClient::new(app);
924
10-
```rust
11-
let client = TestClient::new(app);
25+
let response = client.get("/").await;
26+
27+
response
28+
.assert_status(200)
29+
.assert_body_contains("Hello!");
30+
}
1231
```
1332

14-
## Fluent Assertions
33+
### Testing JSON APIs
34+
35+
The client provides fluent helpers for JSON APIs.
36+
37+
```rust,ignore
38+
#[derive(Serialize)]
39+
struct CreateUser {
40+
username: String,
41+
}
42+
43+
#[tokio::test]
44+
async fn test_create_user() {
45+
let app = RustApi::new().route("/users", post(create_user_handler));
46+
let client = TestClient::new(app);
47+
48+
let response = client.post_json("/users", &CreateUser {
49+
username: "alice".into()
50+
}).await;
51+
52+
response
53+
.assert_status(201)
54+
.assert_json(&serde_json::json!({
55+
"id": 1,
56+
"username": "alice"
57+
}));
58+
}
59+
```
60+
61+
## Mocking Services with `MockServer`
62+
63+
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.
64+
65+
### Setting up a Mock Server
66+
67+
```rust,ignore
68+
use rustapi_testing::{MockServer, MockResponse, RequestMatcher};
69+
70+
#[tokio::test]
71+
async fn test_external_integration() {
72+
// 1. Start the mock server
73+
let server = MockServer::start().await;
74+
75+
// 2. Define an expectation
76+
server.expect(RequestMatcher::new(Method::GET, "/external-api/data"))
77+
.respond_with(MockResponse::new()
78+
.status(StatusCode::OK)
79+
.json(serde_json::json!({ "result": "success" })))
80+
.times(1);
81+
82+
// 3. Configure your app to use the mock server's URL
83+
let app = create_app_with_config(Config {
84+
external_api_url: server.base_url(),
85+
});
86+
87+
let client = TestClient::new(app);
88+
89+
// 4. Run your test
90+
client.get("/my-endpoint-calling-external").await.assert_status(200);
91+
}
92+
```
93+
94+
### Expectations
95+
96+
You can define strict expectations on how your application interacts with the mock server.
97+
98+
#### Matching Requests
99+
100+
`RequestMatcher` allows matching by method, path, headers, and body.
101+
102+
```rust,ignore
103+
// Match a POST request with specific body
104+
server.expect(RequestMatcher::new(Method::POST, "/webhook")
105+
.body_string("event_type=payment_success".into()))
106+
.respond_with(MockResponse::new().status(StatusCode::OK));
107+
```
108+
109+
#### Verification
110+
111+
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.
15112

16-
The client provides a fluent API for making requests and asserting responses.
113+
- `.once()`: Must be called exactly once (default).
114+
- `.times(n)`: Must be called exactly `n` times.
115+
- `.at_least_once()`: Must be called 1 or more times.
116+
- `.never()`: Must not be called.
17117

18-
```rust
19-
client.post("/login")
20-
.json(&credentials)
21-
.send()
22-
.await
23-
.assert_status(200)
24-
.assert_header("Set-Cookie", "session=...");
118+
```rust,ignore
119+
// Ensure we don't call the billing API if validation fails
120+
server.expect(RequestMatcher::new(Method::POST, "/charge"))
121+
.never();
25122
```
26123

27-
## Mocking Services
124+
## Best Practices
28125

29-
Because `rustapi-rs` relies heavily on Dependency Injection via `State<T>`, you can easily inject mock implementations of your database or downstream services when creating the `RustApi` instance for your test.
126+
1. **Dependency Injection**: Design your application `State` to accept base URLs for external services so you can inject the `MockServer` URL during tests.
127+
2. **Isolation**: Create a new `MockServer` for each test case to ensure no shared state or interference.
128+
3. **Fluent Assertions**: Use the chainable assertion methods on `TestResponse` to keep tests readable.

docs/cookbook/src/learning/README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,8 @@ Design and build distributed systems with RustAPI.
6666
| 3 | `rate-limit-demo` | API protection, throttling |
6767
| 4 | `microservices` | Service communication patterns |
6868
| 5 | `microservices-advanced` | Service discovery, Consul integration |
69-
| 6 | Background jobs (conceptual) | Background processing with `rustapi-jobs`, Redis/Postgres backends |
69+
| 6 | Service Mocking | Testing microservices with `MockServer` from `rustapi-testing` |
70+
| 7 | Background jobs (conceptual) | Background processing with `rustapi-jobs`, Redis/Postgres backends |
7071

7172
> Note: The **Background jobs (conceptual)** step refers to using the `rustapi-jobs` crate rather than a standalone example project.
7273
**Related Cookbook Recipes:**
@@ -122,8 +123,10 @@ Build robust, observable, and secure systems.
122123
| 4 | **Optimization** | Configure [Caching and Deduplication](../crates/rustapi_extras.md#optimization) |
123124
| 5 | **Background Jobs** | Implement [Reliable Job Queues](../crates/rustapi_jobs.md) |
124125
| 6 | **Debugging** | Set up [Time-Travel Debugging](../recipes/replay.md) |
126+
| 7 | **Reliable Testing** | Master [Mocking and Integration Testing](../crates/rustapi_testing.md) |
125127

126128
**Related Cookbook Recipes:**
129+
- [rustapi-testing: The Auditor](../crates/rustapi_testing.md)
127130
- [rustapi-extras: The Toolbox](../crates/rustapi_extras.md)
128131
- [Time-Travel Debugging](../recipes/replay.md)
129132
- [rustapi-jobs: The Workhorse](../crates/rustapi_jobs.md)

docs/cookbook/src/recipes/db_integration.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ This recipe shows how to integrate PostgreSQL/MySQL/SQLite using a global connec
88

99
```toml
1010
[dependencies]
11-
sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "postgres", "uuid"] }
11+
sqlx = { version = "0.8", features = ["runtime-tokio", "tls-rustls", "postgres", "uuid"] }
1212
serde = { version = "1", features = ["derive"] }
1313
tokio = { version = "1", features = ["full"] }
1414
dotenvy = "0.15"

0 commit comments

Comments
 (0)