I recently needed to write some unit tests for a bit of code that communicated with a web service, and so I started reading up on Sinatra.
It turns out that Sinatra makes it very simple to generate just about any web service. It is especially trivial to mock data for a given web service end point. Take this example right out of the Sinatra README:
This seemed awesome, but I realized that I had maybe 50 endpoints that I wanted to mock. I guess this still isn't too bad, but I really wanted to generalize my ruby code in such a way that it could mock any endpoint without code modification.
So I wrote a Sinatra service that has only 4 end points using splat captures (*), which is basically a wildcard. I made one route for each of the main HTTP verbs (POST,PUT,GET,DELETE), and had each route match ANY endpoint that matched its verb, meaning:
get '*' do ...
put '*' do ...
post '*' do ...
delete '*' do ...
I then wrote each route to mirror a "fixtures" directory on the filesystem.
For example, say I had an endpoint to some API for getting user information:
GET /companies/company1/users/jdoe
The get '*' route handles this and browses the file on the filesystem at ./fixtures/companies/company1/users/jdoe.json, and returns the contents of this file in the body of the response:
get '*' do
path = Pathname.new("#{File.dirname(__FILE__)}/#{FIXTURESDIR}#{params[:splat].first()}")
pathPlusJson = Pathname.new("#{path}.json")
if path.directory?
response = get_directory_contents_array(path.to_path)
return response.to_json
elsif pathPlusJson.exist? and pathPlusJson.file?
response = JSON.parse(IO.read(pathPlusJson.to_path))
return response.to_json
else
return create_response( {"error" => "Not Found" }.to_json, 404 )
end
end
if path.directory?
response = get_directory_contents_array(path.to_path)
return response.to_json
elsif pathPlusJson.exist? and pathPlusJson.file?
response = JSON.parse(IO.read(pathPlusJson.to_path))
return response.to_json
else
return create_response( {"error" => "Not Found" }.to_json, 404 )
end
end
If I wanted to iterate all users, I could instead request a get on a directory instead of a file:
GET /companies/company1/users
Now the get '*' route browses to the directory and iterates through all the ".json" files returning an array of JSON encoded user objects in the body.
If I want to add a new user I can simply do a POST:
POST /companies/company1/users/dsmith
with some JSON in the body, and this JSON will be written to ./fixtures/companies/company1/dsmith.json
Delete also works as you might expect, deleting the file in the fixtures directory.
All of this can be done with very little code thanks to the domain specific nature of Sinatra. The full ruby code can be found in one of my GitHub Gists's:
https://gist.github.com/jhnbwrs/6e940679a3dd1a23cbc0
# spec/spec_helper.rb RSpec.configure
do |config|
config.before(:each) do
stub_request(:any, /api.company1.com/).to_rack(JSONFileServer)
end
end
If you aren't unit testing a ruby application, you should still be able to fire up this service during the unit test phase of your build, and redirect your API requests to localhost:4567.