Testing
Batman wanted to test his Robyn application without spinning up a full server on every run. Robyn introduced him to the built-in TestClient — a lightweight, in-process test client that executes route handlers directly, making tests fast and deterministic.
Getting Started
The TestClient wraps a Robyn app and lets you call routes as if you were making real HTTP requests — but everything happens in-process. No ports, no sockets, no server startup.
Import TestClient from robyn.testing, pass your app to it, and start making requests:
Request
from robyn import Robyn
from robyn.testing import TestClient
app = Robyn(__file__)
@app.get("/hello")
def hello(request):
return "Hello, World!"
client = TestClient(app)
def test_hello():
response = client.get("/hello")
assert response.status_code == 200
assert response.text == "Hello, World!"
The TestResponse Object
Every request method returns a TestResponse with the following properties:
| Property | Type | Description |
|---|---|---|
status_code | int | HTTP status code |
text | str | Response body as a decoded string |
content | bytes | Raw response body |
headers | Headers | Response headers |
ok | bool | True if status is 2xx |
TestResponse also has a .json() method that parses the body as JSON.
Request
@app.get("/users")
def get_users(request):
return [{"name": "Batman"}, {"name": "Robin"}]
def test_json_response():
response = client.get("/users")
assert response.ok
data = response.json()
assert len(data) == 2
assert data[0]["name"] == "Batman"
HTTP Methods
The TestClient supports all common HTTP methods. Methods that typically send a body (POST, PUT, PATCH, DELETE) accept a json_data parameter for convenience.
Use json_data to send JSON payloads — the client automatically sets Content-Type: application/json and serializes the data:
Request
@app.post("/items")
def create_item(request):
body = request.json()
return {"id": 1, "name": body["name"]}
def test_post_json():
response = client.post("/items", json_data={"name": "Batarang"})
assert response.status_code == 200
assert response.json()["name"] == "Batarang"
You can also send raw string or bytes bodies, custom headers, query parameters, form data, and files:
Request
def test_with_all_options():
response = client.post(
"/search",
body="raw body content",
headers={"X-Custom": "value"},
query_params={"q": "batman"},
)
assert response.ok
Path Parameters
Routes with path parameters work exactly as they do in production. The TestClient matches the route pattern and extracts parameters automatically.
Path parameters are resolved from the URL and passed to your handler through the normal Robyn parameter resolution pipeline:
Request
@app.get("/users/:user_id")
def get_user(request, user_id: int):
return {"user_id": user_id}
def test_path_params():
response = client.get("/users/42")
assert response.json()["user_id"] == 42
Testing Middleware
The TestClient replicates the full request pipeline — before middlewares, the handler, global response headers, and after middlewares — in the same order as the Rust runtime.
Middlewares that modify the request or response are executed just like in production:
Request
@app.before_request()
def add_request_id(request):
request.headers.set("X-Request-ID", "test-123")
return request
@app.after_request()
def add_server_header(response):
response.headers.set("X-Server", "Robyn")
return response
@app.get("/protected")
def protected(request):
return request.headers.get("X-Request-ID")
def test_middleware_pipeline():
response = client.get("/protected")
assert response.text == "test-123"
assert response.headers.get("X-Server") == "Robyn"
Using as a Context Manager
TestClient implements the context manager protocol. When used with with, the internal event loop is automatically cleaned up:
Request
def test_with_context_manager():
with TestClient(app) as client:
response = client.get("/hello")
assert response.ok
# event loop is closed here
Running Tests with pytest
Since TestClient doesn't start a server, tests run as fast as regular unit tests. Use pytest directly — no special plugins or fixtures required.
A typical test file:
Test File
import pytest
from robyn import Robyn
from robyn.testing import TestClient
app = Robyn(__file__)
@app.get("/")
def index(request):
return "Home"
@app.get("/health")
def health(request):
return {"status": "ok"}
@app.post("/echo")
def echo(request):
return request.json()
client = TestClient(app)
def test_index():
assert client.get("/").text == "Home"
def test_health():
data = client.get("/health").json()
assert data["status"] == "ok"
def test_echo():
payload = {"message": "hello"}
response = client.post("/echo", json_data=payload)
assert response.json() == payload
def test_not_found():
response = client.get("/nonexistent")
assert response.status_code == 404
Run with:
pytest test_app.py -v
Available Methods
| Method | Signature |
|---|---|
client.get(path, **kw) | GET request |
client.post(path, json_data=None, **kw) | POST request |
client.put(path, json_data=None, **kw) | PUT request |
client.patch(path, json_data=None, **kw) | PATCH request |
client.delete(path, json_data=None, **kw) | DELETE request |
client.head(path, **kw) | HEAD request |
client.options(path, **kw) | OPTIONS request |
All methods accept these keyword arguments:
| Argument | Type | Description |
|---|---|---|
body | str | bytes | Raw request body |
headers | dict | Request headers |
query_params | dict | Query string parameters |
form_data | dict | Form data fields |
files | dict | File uploads (name → bytes) |
