From Stubbing Frameworks to Real Rack Apps
Service-oriented architecture (SOA) breaks up the monolithic codebase, but it introduces a practical problem: running any single application in isolation now requires standing up a web of external dependencies. At Heroku, where the platform’s core is split into many backend services, the early solution was to stub those services at a high level so developers and test suites could run without remote calls.
The simplest approach was to use a stubbing framework to mock methods that touch external services. That gets tests passing, but it couples the test to the service handler’s internal interface and never exercises the handler logic itself. The next iteration was to design handlers that could mock themselves:
Addons::Client.mock! if ENV["RACK_ENV"] == "test"
That worked for tests, but pushed the burden onto developers to locate a staging instance of the remote service. Even with well-maintained shared staging environments, a broken staging service owned by another team could stall development. The natural follow-up was to build handlers with dual behavior: minimal in-process mocks for dev and test, switching to real remote calls in staging and production:
def detect_handler
return RealHandler.new if ENV.has_key?("CORE_SHUSHU_URL")
MockHandler.new
end
This kind of hybrid reduces setup friction, but it still splits the codebase into two paths: what runs locally is not the same code that runs in production, so any divergence must be caught in staging, where debugging is slower than it would be against a local production copy.
The Rack Stub Pattern
While restructuring the API code, the team moved away from special-purpose stub handlers toward genuine implementations written as Rack-compliant applications. These Rack stubs implement only the subset of the foreign API the calling app relies on, simplified to return just enough for a correct response and little else. A working Sinatra-based stub for a backend service looks like this:
class IonStub < Sinatra::Base
post "/endpoints" do
status 201
content_type :json
MultiJson.encode({
id: 123,
cname: "tokyo-1234.herokussl.com",
elb_dns_name: "elb016353-1923944129.us-east-1.elb.amazonaws.com",
})
end
end
Because the stub is a functioning app in its own right, it is immediately reusable in both development and testing. On a platform with trivial deployment, the same stub can even be pushed to the cloud and shared by staging environments.
Testing Against HTTP
In tests, Rack stubs pair cleanly with WebMock’s Rack support, which intercepts requests aimed at a particular URL and forwards them to the stub. A couple of helpers in the API test suite encapsulate the setup:
# generic helper for use with any service
def stub_service(uri, stub, &block)
uri = URI.parse(uri)
port = uri.port != uri.default_port ? ":\#{uri.port}" : ""
stub = block ? Sinatra.new(stub, &block) : stub
stub_request(:any, /^\#{uri.scheme}:\/\/(.*:.*@)?\#{uri.host}\#{port}\/.*$/).
to_rack(stub)
end
# One-liners specifically for a specific stubs, pointing to configured
# locations of each remote service. A configuration value might look like:
#
# ADDONS_URL=https://api-user:[email protected]
#
def stub_addons
stub_service(ENV["ADDONS_URL"], AddonsStub, &block)
end
def stub_ion(&block)
stub_service(ENV["ION_URL"], IonStub, &block)
end
Once the stub is registered, a test can make a remote call that transparently routes to the stub’s handler:
it "should make a call to ion" do
stub_ion
endpoint = IonAPI.create_endpoint!
end
This is useful where the goal is to exercise as much of the application stack as possible, since the point of stubbing is this layer—HTTP rather than a local service library—means nearly all production code runs. Error scenarios are also easy: extend the stub with Sinatra’s DSL for a particular test case and hit the failure path.
it "should raise an error on a bad ion response" do
stub_ion do
post("/endpoints") { 422 }
end
lambda do
IonAPI.create_endpoint!
end.should raise_error(IonAPI::Error)
end
Running Stubs Locally
Each Rack stub includes a small conditional block so it can boot as an app on its own. Invoking the filename directly starts Sinatra:
class IonStub < Sinatra::Base
..
end
if __FILE__ == $0
$stdout.sync = $stderr.sync = true
IonStub.run! port: 5100
end
$ ruby test/test_support/service_stubs/ion_stub.rb
>> Listening on 0.0.0.0:5100, CTRL+C to stop
With the stub running, an environment variable points the main application at it during development. Once configured, the main app boots and requests flow through to the service stub, mimicking part of a larger service mesh with minimal ceremony. In practice, this means a request can provision a resource as if it were hitting the real infrastructure:
$ export HEROKU_HOST=http://localhost:5000
$ heroku addons:add ssl:endpoint
$ heroku certs:add secure.example.org.pem secure.example.org.key
Adding SSL Endpoint to great-cloud... done
WARNING: ssl_cert provides no domain(s) that are configured for this Heroku app
great-cloud now served by tokyo-1234.herokussl.com
Certificate details:
Common Name(s): alt1.example.org
alt2.example.org
secure.example.org
Expires At: 2031-05-05 19:05 UTC
Issuer: /C=US/ST=California/L=San Francisco/O=Heroku/CN=secure.example.org
Starts At: 2011-05-10 19:05 UTC
Subject: /C=US/ST=California/L=San Francisco/O=Heroku/CN=secure.example.org
SSL certificate is self signed.
Booting stubs by hand works, but Foreman automates it. Add stubs to the Procfile alongside the main app, and put the stub addresses in the local .env:
web: bundle exec puma --quiet --threads 8:32 --port 5000 config.ru
# stubs
ionstub: bundle exec ruby service_stubs/ion_stub.rb
ION_URL=http://localhost:5100
A single foreman start now brings up the whole cluster:
18:18:22 web.1 | listening on addr=0.0.0.0:5000 fd=13
18:18:22 ionstub.1 | == Sinatra/1.3.5 has taken the stage on 5100 for development with backup from WEBrick
The convenience compounds as the roster of stubs grows. With many backend services registered in the process list, a single command boots a complete development stack, closely approximating production’s topology through actual HTTP traffic. A runnable example of the project is available from brandur/service-stub-example.



