Mark Mzyk March 5, 2012
RSpec, a Behavior Driven Development framework, is great for various kinds of testing, such as unit and integration testing. However one area that is hard to test is integrations that cross APIs. You call into a service, then need to wait while it takes some action before proceeding with the test you want. An example of this would be inserting an item into a search service, then querying the service to test the output, ensuring your item is there. The service might have a queue and so doesn’t insert the item immediately. How do you test this?
One way is to call the API, sleep, and then run the test. The problem with this is that you have to wait the entire sleep duration for every test run, even if the results are ready before the sleep is done. RSpec’s and Ruby’s flexibility allow you to avoid this situation by writing a helper function:
def wait(time, increment = 5, elapsed_time = 0, &block)
rescue Exception => e
if elapsed_time >= time
wait(time, increment, elapsed_time + increment, &block)
To use this function, you pass your test to it as a block. The wait function will immediately try the test. If a failure is reported – which in RSpec is just an exception – the function catches the failure and sleeps. The assumption is that a failure means the API that is called into has not yet completed the action that is being waited upon. The wait function continues to retry the test until the test either passes or until it reaches the timeout that was passed to it, at which point it will raise the last error it caught, which will be the last failure of the test it ran.
The wait function has the benefit of returning successes as soon as they are encountered, so the tests don’t have to sleep for the entire duration specified. However, it still has to wait the entire sleep period for a failure, because it can’t distinguish a legitimate failure from the API service not yet returning the correct results.
Using the wait function in an RSpec test looks like this:
describe "a test" do
before :each do
after :each do
it "should receive the results it expects from the api" do
wait 30 do
search_for_user_in_remote_service("user").should == "user"