From Loose Stubs to Schema-Validated Mocks
Service stubs are useful for keeping a distributed architecture testable: they let an app exercise its full stack, from handlers down to real HTTP calls, without needing the actual remote component. But those stubs only approximate the real service, and low-fidelity fake data can let type errors and malformed requests slip through to production.
The fix is to pair the stub with the same JSON Schema that describes the real service's API. We're using Committee, a small middleware library that validates incoming requests against a supplied schema, to make local stubs behave more like the production systems they're standing in for.
post "/apps" do
content_type :json
status 201
id = SecureRandom.uuid
JSON.pretty_generate({
id: id,
name: "app-#{id}",
})
end
Tighter Constraints, Faster Failure
With a schema in place, a stub built as a small Sinatra app can reject bad requests the same way the real endpoint would. For instance, if the schema defines a name parameter as a string with a specific format, sending an integer gets you a validation error immediately:
# will validate input parameters
use Committee::Middleware::RequestValidation,
schema: File.read("schema.json")
post "/apps" do
...
end
Here's the schema fragment that enforces that constraint:
"definitions": {
"name": {
"description": "unique name of app",
"example": "example",
"pattern": "^[a-z][a-z0-9-]{3,50}$",
"readOnly": false,
"type": [
"string"
]
},
...
},
"links": [
{
"description": "Create a new app.",
"href": "/apps",
"method": "POST",
"rel": "create",
"schema": {
"properties": {
"name": {
"$ref": "#/definitions/app/definitions/name"
}
},
"type": [
"object"
]
},
"title": "Create"
},
...
]
And here's what happens when you hit the stub with a request that passes an integer where a string is expected:
$ curl -i http://localhost:5000/apps -X POST \
-H "Content-Type: application/json" -d '{"name":123}'
HTTP/1.1 422
Content-Type: application/json
X-Content-Type-Options: nosniff
Server: WEBrick/1.3.1 (Ruby/1.9.3/2012-04-20)
Date: Wed, 30 Apr 2014 03:49:34 GMT
Content-Length: 106
Connection: Keep-Alive
{
"id": "invalid_params",
"error": "Invalid type for key \"name\": expected 123 to be [\"string\"]."
}
Because the stub remains a standard Sinatra app, you can still layer on business rules and edge-case behaviors that a JSON Schema can't express:
use Committee::Middleware::RequestValidation,
schema: File.read("schema.json")
post "/apps" do
content_type :json
if (8..17).include?(Time.now.hour)
status 422
JSON.pretty_generate(
message: "Can't create apps outside of business hours!"
)
end
...
end
Symmetry in Validation
Schema-driven validation isn't just for stubs. Applying the same middleware to the actual service implementation can drastically cut parameter-checking boilerplate: once a request reaches the handler, the code can safely assume the payload is well-formed and typed correctly.
That creates a useful symmetry across the architecture. A component validates incoming requests against the exact same interface definition that other components use to validate their requests to it. As a result, simple integration tests catch far more mistakes early, before they become time-consuming issues in a fully wired-up system.
The complete example is available on GitHub.



