Description
FirePoll.poll
is a method for knowing when something is ready. When your block yields true, execution continues. When your block yields false, poll keeps trying until it gives up and raises an error.
FirePoll.patiently
extends this idea to letting your assertion(s) achieve success after a few tries, if necessary.
Examples: poll
I'm writing a system test for a web application. My test simulates uploading a large file, which isn't instantaneous. I need to know when the file has finished uploading so I can start making assertions.
def wait_for_file(filename)
FirePoll.poll do
FileStore.file_ready?(filename)
end
end
def test_files_are_saved
upload_file "blue.txt"
assert_nothing_raised { wait_for_file "blue.txt" }
assert_equal "blue is the best color", read_saved_file("blue.txt")
end
I just fired up a fake web service to respond to my client application. I want to know when the service is online before I start the client tests. I'm willing to wait 10 seconds before giving up.
class TestHelper
include FirePoll
def wait_for_server
poll("server didn't come online quick enough", 10) do
begin
TCPSocket.new(SERVER_IP, SERVER_PORT)
true
rescue Exception
false
end
end
end
end
Example: patiently
I'm writing tests for my web app which uses a bunch of crazy Ajax to fetch data from a service and populate a table... one row at a time. In real life it takes just a moment to complete, but sometimes one or two of the rows hang for a second before continuing.
it "loads the tasks asynchronously and fills the table" do
go_to_task_list_page
patiently do
read_task_table_row(1).should == [ "Ride bike", "Done" ]
read_task_table_row(2).should == [ "Write code", "Done" ]
read_task_table_row(3).should == [ "Go to The Meanwhile", "Todo" ]
end
end
This test clearly shows what you're interested in, without getting tripped up by delayed Ajax results, but without adding unneeded synchronization or sleep code.
Usage
Pass a block to FirePoll.poll
. Return true
when your need is met. Return false
when it isn't. poll
will raise an exception after too many failed attempts.
The poll
method takes two optional parameters: a specific message to raise on failure and the number of seconds to wait. By default, poll
will try for two seconds. poll
runs every tenth of a second.
FirePoll.poll { ... } # raises an error after two seconds
FirePoll.poll("new data hasn't arrived from the device") { ... } # raises a friendlier error message
FirePoll.poll("waited for too long!", 7) { ... } # raises an error after seven seconds with a specific error message
FirePoll.poll(nil, 88) { ... } # raises an error after eighty-eight seconds with the generic error message
FirePoll.patiently
is similar, but instead focuses on error-free execution of arbitrary code or tests. If the passed block runs without raising an error, execution proceeds normally. If an error is raised, the block is rerun after a brief delay, until the block can be run without exceptions. If exceptions continue to raise, patiently
gives up after a bit (default 5 seconds) by re-raising the most recent exception raised by the block.
The FirePoll
module may be mixed into your class via include
for nicer reading.
FirePoll.poll { ... } # returns immedialtely if no errors, or as soon as errors stop
FirePoll.poll(10) { ... } # increase patience to 10 seconds
FirePoll.poll(20, 3) { ... } # increase patience to 20 seconds, and delay for 3 seconds before retry
RSpec
We tend to include the FirePoll module up-front for all our specs:
RSpec.configure do |config|
config.include FirePoll
...
end
Implementation
poll
and patiently
are both wall-clock sensitive now, meaning they will not poll longer than their allotted time. This means if your blocks spend significant time determining truth or success, these methods no longer suffer from the multiplicative effects of up-front loop-count calculation.
Motivation
We frequently need to wait for something to happen - usually in tests. And we usually don't have any strict time requirements - as long as something happens in about [x] seconds, we're happy. poll
and patiently
are cover a lot of ground quickly and cleanly.
On a related note, Timer::Timeout
is known to be busted and unreliable. FirePoll.poll
doesn't employ any threads or timers, so we don't worry about whether it will work or not.
TODO
- Nice to have: hook into Test::Unit and RSpec instead of raising a Ruby exception
- Nice to have: pass options as a hash instead of two parameters. This will look nice with Ruby 1.9's hash syntax.
Authors
- Matt Fletcher (fletcher@atomicobject.com)
- David Crosby (crosby@atomicobject.com)
- Micah Alles (alles@atomicobject.com)
- © Atomic Object
- More Atomic Object open source projects