rustapi-testing: The Auditor
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:
- In-process API testing: Testing your endpoints without binding to a real TCP port.
- External service mocking: Mocking downstream services (like payment gateways or auth providers) that your API calls.
Installation
Add the crate to your dev-dependencies:
[dev-dependencies]
rustapi-testing = { version = "0.1.335" }
The TestClient
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
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);
let response = client.get("/").await;
response
.assert_status(200)
.assert_body_contains("Hello!");
}
Testing JSON APIs
The client provides fluent helpers for JSON APIs.
#[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
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.
// Match a POST request with specific body
server.expect(RequestMatcher::new(Method::POST, "/webhook")
.body_string("event_type=payment_success".into()))
.respond_with(MockResponse::new().status(StatusCode::OK));
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.
.once(): Must be called exactly once (default)..times(n): Must be called exactlyntimes..at_least_once(): Must be called 1 or more times..never(): Must not be called.
// Ensure we don't call the billing API if validation fails
server.expect(RequestMatcher::new(Method::POST, "/charge"))
.never();
Best Practices
- Dependency Injection: Design your application
Stateto accept base URLs for external services so you can inject theMockServerURL during tests. - Isolation: Create a new
MockServerfor each test case to ensure no shared state or interference. - Fluent Assertions: Use the chainable assertion methods on
TestResponseto keep tests readable.