RSpec::Wait
Time-resilient expectations in RSpec
Why does RSpec::Wait exist?
Timing is hard.
Timing problems and race conditions can plague your test suite. As your test suite slowly becomes less reliable, development speed and quality suffer.
RSpec::Wait strives to make it easier to test asynchronous or slow interactions.
How does RSpec::Wait work?
RSpec::Wait allows you to wait for an assertion to pass, using the RSpec syntactic sugar that you already know and love.
RSpec::Wait will keep trying until your assertion passes or times out.
Examples
RSpec::Wait's wait_for
assertions are nearly drop-in replacements for RSpec's
expect
assertions. The major difference is that the wait_for
method
requires a block because it may need to evaluate the content of that block
multiple times while it's waiting.
RSpec.describe Ticker do
subject(:ticker) { Ticker.new("foo") }
describe "#start" do
before do
ticker.start
end
it "starts with a blank tape" do
expect(ticker.tape).to eq("")
end
it "sends the message in Morse code one letter at a time" do
wait_for { ticker.tape }.to eq("··-·")
wait_for { ticker.tape }.to eq("··-· ---")
wait_for { ticker.tape }.to eq("··-· --- ---")
end
end
end
RSpec::Wait can be especially useful for testing user interfaces with tricky timing elements like JavaScript interactions or remote requests.
feature "User Login" do
let!(:user) { create(:user, email: "john@example.com", password: "secret") }
scenario "A user can log in successfully" do
visit new_session_path
fill_in "Email", with: "john@example.com"
fill_in "Password", with: "secret"
click_button "Log In"
wait_for { current_path }.to eq(account_path)
expect(page).to have_content("Welcome back!")
end
end
Compatibility
Ruby Support
RSpec::Wait is tested against all non-EOL Ruby versions, which as of this writing are versions 3.1, 3.2, and 3.3. If you find that RSpec::Wait does not work or is not tested for a maintained Ruby version, please open an issue or pull request to add support.
Additionally, RSpec::Wait is tested against Ruby head to surface future compatibility issues, but no guarantees are made that RSpec::Wait will function as expected on Ruby head. Proceed with caution!
RSpec Support
RSpec::Wait is tested against several versions of RSpec, which as of this writing are versions 3.4 through 3.13. If you find that RSpec::Wait does not work or is not tested for a newer RSpec version, please open an issue or pull request to add support.
Additionally, RSpec::Wait is tested against unbounded RSpec to surface future compatibility issues, but no guarantees are made that RSpec::Wait will function as expected on any RSpec version that's not explicitly tested. Proceed with caution!
Matchers
RSpec::Wait ties into RSpec's internals so it can take full advantage of any
matcher that you would use with RSpec's own expect
method.
If you discover a matcher that works with expect
but not with wait_for
,
please open an issue
and I'd be happy to take a look!
Installation
To get started with RSpec::Wait, simply add the dependency to your Gemfile
and bundle install
:
gem "rspec-wait", "~> 1.0"
If your codebase calls Bundler.require
at boot time, you're all set and the
wait_for
method is already available in your RSpec suite.
If you encounter the following error:
NoMethodError:
undefined method `wait_for'
You will need to explicitly require RSpec::Wait at boot time in your test environment:
require "rspec/wait"
Upgrading from v0
RSpec::Wait v1 is very similar in syntax to v0 but does have a few breaking changes that you should be aware of when upgrading from any 0.x version:
- RSpec::Wait v1 requires Ruby 3.0 or greater and RSpec 3.4 or greater.
- The
wait_for
andwait.for
methods no longer accept arguments, only blocks. - RSpec::Wait no longer uses Ruby's problematic
Timeout.timeout
method, which means it will no longer raise aRSpec::Wait::TimeoutError
. RSpec::Wait v1 never interrupts the block given towait_for
mid-call so make every effort to reasonably limit the block's individual call time.
Configuration
RSpec::Wait has three available configuration values:
-
wait_timeout
- The maximum amount of time (in seconds) that RSpec::Wait will continue to retry a failing assertion. Default:10.0
-
wait_delay
- How long (in seconds) RSpec::Wait will pause between retries. Default:0.1
-
clone_wait_matcher
- Whether each retry willclone
the given RSpec matcher instance for each evaluation. Set totrue
if you have trouble with a matcher holding onto stale state. Default:false
RSpec::Wait configurations can be set in three ways:
- Globally via
RSpec.configure
- Per example or context via RSpec metadata
- Per assertion via the
wait
method
Global Configuration
RSpec.configure do |config|
config.wait_timeout = 3 # seconds
config.wait_delay = 0.5 # seconds
config.clone_wait_matcher = true
end
RSpec Metadata
Any of RSpec::Wait's three configurations can be set on a per-example or
per-context basis using wait
metadata. Provide a hash containing any
number of shorthand keys and values for RSpec::Wait's configurations.
scenario "A user can log in successfully", wait: { timeout: 3, delay: 0.5, clone_matcher: true } do
visit new_session_path
fill_in "Email", with: "john@example.com"
fill_in "Password", with: "secret"
click_button "Log In"
wait_for { current_path }.to eq(account_path)
expect(page).to have_content("Welcome back!")
end
The wait
Method
And on a per-assertion basis, the wait
method accepts a hash of shorthand
keys and values for RSpec::Wait's configurations. The wait
method must be
chained to the for
method and aside from the ability to set RSpec::Wait
configuration for the single assertion, it behaves identically to wait_for
.
scenario "A user can log in successfully" do
visit new_session_path
fill_in "Email", with: "john@example.com"
fill_in "Password", with: "secret"
click_button "Log In"
wait(timeout: 3).for { current_path }.to eq(account_path)
expect(page).to have_content("Welcome back!")
end
The wait
method will also accept timeout
as a positional argument for
improved readability:
wait(3.seconds).for { current_path }.to eq(account_path)
Use with RuboCop
If you use rubocop
and rubocop-rspec
in your codebase, an RSpec example
with a single wait_for
assertion may cause RuboCop to complain:
RSpec/NoExpectationExample: No expectation found in this example.
By default, RuboCop sees only expect*
and assert*
methods as expectations.
You can configure RuboCop to recognize wait_for
and wait.for
as
expectations (in addition to the defaults) in your RuboCop configuration:
RSpec/NoExpectationExample:
AllowedPatterns:
- ^assert_
- ^expect_
- ^wait(_for)?$
Of course, you can always disable this cop entirely:
RSpec/NoExpectationExample:
Enabled: false
Use with Cucumber
To enable RSpec::Wait in your Cucumber step definitions, add the following to
features/support/env.rb
:
require "rspec/wait"
World(RSpec::Wait)
Who wrote RSpec::Wait?
My name is Steve Richert and I wrote RSpec::Wait in April, 2014 with the support of my employer, Collective Idea. RSpec::Wait owes its current and future success entirely to inspiration and contribution from the Ruby community, especially the authors and maintainers of RSpec.
Thank you! 💛
How can I help?
RSpec::Wait is open source and contributions from the community are encouraged! No contribution is too small.
See RSpec::Wait's contribution guidelines for more information.
If you're enjoying RSpec::Wait, please consider sponsoring my open source work! 💚