patiently retries a block of code until it stops raising an exception (or
returns a truthy value). It is most useful in tests that need to wait for an
eventually-consistent condition — a background job to finish, an AJAX request to
update the DOM, a file to appear — without sprinkling sleep calls everywhere.
It has no runtime dependencies: it does not require RSpec, Capybara, Rails or ActiveSupport.
Add it to your Gemfile and run bundle install:
gem "patiently"Patiently does not depend on RSpec, but if you use it, wire the helpers into your specs. Either require the bundled integration, which includes the helpers into feature and system specs:
# spec/spec_helper.rb (or rails_helper.rb)
require "patiently/rspec"or include the helpers yourself with whatever scope you like:
RSpec.configure do |config|
config.include(Patiently::Helpers, type: :feature)
config.include(Patiently::Helpers, type: :system)
endMix the helpers into any class:
include Patiently::HelpersThis gives you two methods.
Runs the block and, if it raises, keeps retrying until it succeeds or the patience window is exhausted. On success it returns the block's value; on failure it re-raises the block's last exception.
patiently do
expect(page).to have_content("Saved!")
endpatiently retries on any exception (including non-StandardErrors such as
RSpec's ExpectationNotMetError), which is what makes it work with test
assertions.
You can pass a custom timeout (in seconds) as the first argument, overriding the global default:
patiently(10) do
expect(page).to have_content("Slow import finished")
endRetries the block until it returns a truthy value, then returns true. If the
block is still falsey when the window elapses, it returns false. A real
exception raised inside the block still propagates.
patiently_until { user.reload.confirmed? } # => true / falseThis is handy for building custom matchers that retry internally while keeping their own failure message. It also accepts a custom timeout:
patiently_until(10) { import.reload.done? }patiently_wait_until is available as an alias for patiently_until.
When you call patiently (or patiently_until) while already inside a
patiently block on the same thread, the inner block simply runs once — only the
outermost block is retried. This prevents an inner retry loop from repeatedly
re-running expensive setup and lets the outer block drive the timing.
Configure global defaults via Patiently.config:
Patiently.config.timeout = 5 # seconds before giving up
Patiently.config.retry_intervals = [0.05] # sleep durations between retries
Patiently.config.min_retries = 1 # always retry at least this often
Patiently.config.max_retries = nil # cap on retries (nil = unlimited)Or use a block:
Patiently.configure do |config|
config.timeout = 10
end| Option | Default | Meaning |
|---|---|---|
timeout |
5 |
How long (in seconds) to keep retrying before giving up. |
retry_intervals |
[0.05] |
Sleep durations (seconds) between retries. See "Backoff" below. |
min_retries |
1 |
The minimum number of retries performed before giving up, even if the timeout has already elapsed. |
max_retries |
nil |
The maximum number of retries before giving up, regardless of the timeout. nil means unlimited. |
Both min_retries and max_retries count retries — re-invocations after
the first call. So min_retries = 1 means the block runs at least twice before
patiently is allowed to give up.
patiently gives up (and re-raises the block's exception) when either:
- the timeout has elapsed and at least
min_retriesretries have happened, or max_retriesretries have happened (whenmax_retriesis set).
retry_intervals may be an array, used as a backoff schedule. The value at index
N is the sleep before the (N+1)-th retry; once the array is exhausted, its last
element is reused for all further retries:
Patiently.config.retry_intervals = [0.05, 0.05, 0.05, 0.1]
# sleeps 0.05, 0.05, 0.05, then 0.1, 0.1, 0.1, ...If the monotonic clock does not advance between retries — which usually means time
has been mocked or frozen (Timecop, Rails' travel/freeze_time, …) —
patiently would loop forever. Instead it raises Patiently::FrozenInTime so you
notice and travel time explicitly.
Patiently::Error— base class for all errors raised by this gem.Patiently::FrozenInTime— raised when time appears to be frozen (see above).
After checking out the repo, run bin/setup to install dependencies, then run
rake spec to run the tests. You can also run bin/console for an interactive
prompt.
To release a new version, update the version number in
lib/patiently/version.rb, then run bundle exec rake release.
Bug reports and pull requests are welcome on GitHub at https://github.com/makandra/patiently.
The gem is available as open source under the terms of the MIT License.