Automatic Testing of REST Web Services Client with Rails
Testing REST web services client has never been easy. It requires a running web server, multiple threads, network conection and complex transaction management.
Ideally, REST web service client test should have the following characteristics:
- The experience of testing REST resource is similar to that of testing a ActiveRecord model
- Start up and shut down web server for the purpose of running REST web services
- Rollback test data after each test
- Control fixture creation for REST web services
- All tests are automatic
In this article, I demonstrate solutions to each of those mentioned.
As an example throughout the article, let’s assume we have web services for a model called Task and we are testing its corresponding client code. Here is a sample action in the TasksController of the web server:
# server/app/controllers/tasks_controller.rb
def index
@tasks = Task.all
render :status => :ok, :json => @tasks
end
We render @tasks as the JSON format where to_json is called on the object. When you run “curl http://localhost:3000/tasks.json”, you will get the following result:
$ curl http://localhost:3000/tasks.json
[{"id":1,"name":"Write a blog post","created_at":"2011-07-20T04:05:41Z","updated_at":"2011-07-20T04:05:41Z","ends_at":"2011-08-20T03:15:00Z"}]
ActiveResource
In order to test our REST web services, we need a HTTP client. There are lots of them out there, but I found ActiveResource the most enjoyable to use in a less complex situation. ActiveResource provides ActiveRecord compatible APIs, so when writing web service client tests, we feel like we are writing unit tests for a ActiveRecord model.
To start with, we just need to extend it from ActiveResource::Base and give it the web server URL and representation format. That’s it!
# client/app/models/task.rb
class Task < ActiveResource::Base
self.site = "http://localhost:3000"
self.format = :json
end
And we are using it as if you are using an ActiveRecord object:
# client/spec/models/task_spec.rb
describe Task do
it "should return all the tasks" do
@tasks = Task.all
@tasks.size.should == 1
end
end
Web Server
To maintain a zero-setup test environment, we’ll have our test control the stratup and shutdown of a web server. By having the tests start and stop the web server, they can be easily run with no external dependencies.
To control the startup and shutdown of a web server before and after all suites run, it’s as simple as having something like this:
# client/spec_helper.rb
RSpec.configure do |config|
config.before :suite do
@server = Server.new(server_path)
@server.start
end
config.after :suite do
@server.stop
end
end
The implementation of Server is also dead simple. Execute “script/rails server -d” to daemonize the server and issue a kill to stop it:
# client/lib/server.rb
class Server
def initialize(server_path)
@server_path = server_path
end
def start
`#{rails_script} server -d -e test`
end
def stop
pid = File.read(pidfile)
`kill -9 #{pid}`
end
private
def rails_script
File.join(@server_path, 'script', 'rails')
end
def pidfile
File.join(@server_path, 'tmp', 'pids', 'server.pid')
end
end
Transaction Rollback
For testing strategies of web services, most people recommend to either truncate test data on each run or to mock out the request and response. These approaches are less ideal since they’re either less effective or they’re not testing full stack of the targeted web services.
Would it be possible to wrap web services calls in a transaction and rollback data after each test, like what Rails’s transactional fixture does?
Of course! But let’s first try to understand why making transaction rollback for web service calls is difficult:
-
Tests and web server are running in two separate threads, web server’s transactional boundary can’t expand to tests
-
Web service calls may commit its transaction
-
Web server doesn’t know when to rollback the test data
To overcome these problems, we’ll need to fully control the lifecycle of web server’s database connection in the client tests. But how are we able to do this in a client-server architecture?
dRuby to rescue!
For those who are not familiar with it, dRuby is as the Remote Method Invocation to Java as to Ruby. It allows methods to be called in one Ruby process upon a Ruby object located in another Ruby process. Here is a good introduction to brush you up.
We’ll make use of dRuby to directly control the lifecycle of web service’s database connection (ActiveRecord::Base.connection) in our web services client tests. To do that, we add the following code to web server’s “config/environments/test.rb”:
# server/config/environments/test.rb
config.after_initialize do
ActiveRecord::ConnectionAdapters::ConnectionPool.class_eval do
alias_method :old_checkout, :checkout
def checkout
@cached_connection ||= old_checkout
end
end
require 'drb'
DRb.start_service("druby://localhost:8000", ActiveRecord::Base)
end
The above code snippet does two things:
- Patch ActiveRecord::ConnectionAdapters::ConnectionPool#checkout to make sure only one connection is shared across threads
- Start a dRuby service for ActiveRecord::Base to be used in tests
In case you are wondering why it’s necessary to share one database connection across threads: ActiveRecord creates one database connection for each thread in its connection pool. Our web service client tests run in a separate thread from the server’s so it’s impossible to track which connection to rollback data for the web services calls. What we are doing here is to make sure there is only one connection created and we always rollback data for this connection.
After the aforementioned setup, we are able to expand the transaction boundary to tests:
# client/spec/models/task_spec.rb
describe Task do
before :all do
@semaphore = Mutex.new
DRb.start_service
@remote_base = DRbObject.new nil, "druby://localhost:8000"
end
before :each do
begin_remote_transaction
end
after :each do
rollback_remote_transaction
end
it "creates a task through web serices" do
task = Task.create(:name => "Write a blog post", :ends_at => Date.tomorrow)
Task.find(task.id).should == task
end
private
def begin_remote_transaction
@semaphore.lock
@remote_base.connection.increment_open_transactions
@remote_base.connection.transaction_joinable = false
@remote_base.connection.begin_db_transaction
end
def rollback_remote_transaction
@remote_base.connection.rollback_db_transaction
@remote_base.connection.decrement_open_transactions
@remote_base.clear_active_connections!
@semaphore.unlock
end
end
Voila! With dRuby, we use begin+rollback to isolate changes of web services calls to the database, instead of having to delete+insert for every test case. A huge performance boost!
Note that the Mutex lock in the code is to make sure that multiple web service client tests can run concurrently, for exmaple, using the parallel_tests gem. Without this lock, while the remote ActiveRecord connection is shared, the tests will behavior strangely in a multi-threads environment. You can ignore those lines if your web service client tests never run concurrently.
We can easily refactor out the begin_remote_transaction method and the rollback_remote_transaction method to spec_helper.rb, so that our web services client tests have little difference from usual ActiveRecord unit tests.
# client/spec_helper.rb
RSpec.configure do |config|
config.before :all do
@semaphore = Mutex.new
DRb.start_service
@remote_base = DRbObject.new nil, "druby://localhost:8000"
end
config.before :each do
@semaphore.lock
@remote_base.connection.increment_open_transactions
@remote_base.connection.transaction_joinable = false
@remote_base.connection.begin_db_transaction
end
config.after :each do
@remote_base.connection.rollback_db_transaction
@remote_base.connection.decrement_open_transactions
@remote_base.clear_active_connections!
@semaphore.unlock
end
end
# client/spec/models/task_spec.rb
describe Task do
it "creates a task through web serices" do
task = Task.create(:name => "Write a blog post", :ends_at => Date.tomorrow)
Task.find(task.id).should == task
end
end
Fixture Creation
Most of the time, we create test fixtures to quickly define prototypes for each of the models and ask for instances with properties that are important to the test at hand. But in the context of REST web services, we can’t create fixtures unless there is a REST API defined. To break this constraint, we use dRuby to open up another channel to directly interact with fixture data on web server.
Assuming we are using the factory_girl gem for fixture creation, We create a dRuby service for port discovery and a dRuby service for each fixture instance:
# server/lib/drb_active_record_instance_factory.rb
require 'factory_girl'
class DRbActiveRecordInstanceFactory
def get_port_for_fixture_instance(factory_instance)
port = create_port
inst = Factory.create(factory_instance)
DRb.start_service("druby://localhost:#{port}", inst)
port
end
def create_port
# create a random port
end
end
DRb.start_service('druby://localhost:9000', DRbActiveRecordInstanceFactory.new)
In tests, we ask for the port of the fixture instance and query its corresponding remote reference:
# client/spec/models/task_spec.rb
describe Task do
before :all do
@drb_factory = DRbObject.new(nil, 'druby://localhost:9000')
end
before do
remote_task_port = @drb_factory.get_port_for_fixture_instance(:task)
@remote_task = DRbObject.new(nil, "druby://localhost:#{remote_task_port}")
end
it "should ..."
# test REST web services calls with @remote_task
end
end
Summary
Testing REST web services client can be less complex if we have full control over objects on the web server. ActiveResource and dRuby stand out to help! They make writing web service client tests feel like writing local unit tests.