Using RSpec Feature Tests to Actually Test What You Think You're Testing
I can't rest. I must know.
How did has_tag
wrap assert_select
? Why, and when, was it removed from RSpec?
Let's start confirming where and when it did exist.
A little Googling reveals that it's currently present in a gem called rspec-html-matchers
, whose "About" says
Old school have_tag, with_tag(and more) matchers for rspec 3 (Nokogiri powered)
In the README, we see
syntax is similar to have_tag
matcher from rspec-rails 1.x, but with own syntactic sugar
Okay, so let's go look at the release notes for rspec-rails 2.0!
Oh... there aren't any.
There is, however, a document on upgrading from rspec-rails-1.x to rspec-rails-2 that explains the situation quite nicely.
Before Webrat came along, rspec-rails had its ownhave_tag
matcher that wrapped Rails'assert_select
. Webrat included a replacement forhave_tag
as well as new matchers (have_selector
andhave_xpath
), all of which rely on Nokogiri to do its work, and are far less brittle than RSpec'shave_tag
.
Capybara has similar matchers, which will soon be available view specs (they are already available in controller specs with render_views
).
Given the brittleness of RSpec'shave_tag
matcher and the presence of new Webrat and Capybara matchers that do a better job,have_tag
was not included in rspec-rails-2.
This begins to explain at least some of my trouble... because render_views
isn't available in a request
spec. That's a controller thing. According to the rspec-rails documentation, capybara matchers aren't available on request specs, either – those are for feature specs.
So... let's try writing a feature spec! I'm not sure this is the "right" way to test the behavior in question ("does the title tag get generated correctly") but trying it out will help me understand the tradeoffs.
First, can I just swap out type "request" for type "feature?"
require 'rails_helper'
RSpec.describe "StaticPages", type: :feature do
describe "GET /home" do
it "returns http success" do
get "/static_pages/home"
expect(response).to have_http_status(:success)
expect(response).to have_title("lupus")
end
end
describe "GET /help" do
it "returns http success" do
get "/static_pages/help"
expect(response).to have_http_status(:success)
end
end
end
Nope!
Failures:
1) StaticPages GET /home returns http success
Failure/Error: get "/static_pages/home"
NoMethodError:
undefined method `get' for #<RSpec::ExampleGroups::StaticPages::GETHome:0x00007fde585414b8>
Did you mean? gets
gem
# ./spec/requests/static_pages_spec.rb:7:in `block (3 levels) in <top (required)>'
2) StaticPages GET /help returns http success
Failure/Error: get "/static_pages/help"
NoMethodError:
undefined method `get' for #<RSpec::ExampleGroups::StaticPages::GETHelp:0x00007fde3de77ea8>
Did you mean? gets
gem
# ./spec/requests/static_pages_spec.rb:15:in `block (3 levels) in <top (required)>'
Finished in 0.01229 seconds (files took 0.88052 seconds to load)
4 examples, 2 failures, 2 pending
Fair, RSpec. Entirely fair.
Let's try something a little more feature-y
require 'rails_helper'
RSpec.describe "StaticPages", type: :feature do
describe "GET /home" do
it "returns http success" do
visit "/static_pages/home"
expect(page).to have_title("lupus")
end
end
describe "GET /help" do
it "returns http success" do
visit "/static_pages/help"
expect(page).to have_title("lupus")
end
end
end
That's more like it!
Failures:
1) StaticPages GET /home returns http success
Failure/Error: expect(page).to have_title("lupus")
expected "Home | Ruby on Rails Tutorial Sample App" to include "lupus"
# ./spec/requests/static_pages_spec.rb:8:in `block (3 levels) in <top (required)>'
2) StaticPages GET /help returns http success
Failure/Error: expect(page).to have_title("lupus")
expected "Help | Ruby on Rails Tutorial Sample App" to include "lupus"
# ./spec/requests/static_pages_spec.rb:15:in `block (3 levels) in <top (required)>'
Finished in 0.09466 seconds (files took 0.85032 seconds to load)
4 examples, 2 failures, 2 pending
These tests are even a bit faster than the rails controller tests that the tutorial has been having us write.
➜ sample_app git:(rspec) ✗ rails test
Running via Spring preloader in process 4833
Run options: --seed 15658
# Running:
E
Error:
StaticPagesControllerTest#test_should_get_about:
ActionController::MissingExactTemplate: StaticPagesController#about is missing a template for request formats: text/html
test/controllers/static_pages_controller_test.rb:19:in `block in <class:StaticPagesControllerTest>'
rails test test/controllers/static_pages_controller_test.rb:18
..
Finished in 0.204258s, 14.6873 runs/s, 29.3746 assertions/s.
3 runs, 6 assertions, 0 failures, 1 errors, 0 skips
The best part, though, is that this solves entirely the problem that had me awkwardly writing
assert_select "title", "Home | Ruby on Rails Tutorial Sample App"
assert_select "title", 1
in the previous test style.
With the assertion
expect(page).to have_title("lupus")
this passes
<title>lupus</title>
<title>BingoDingo</title>
but the reverse doesn't.
<title>BingoDingo</title>
<title>lupus</title>
It looks like this test is sensitive to the actual page title, not just the presence of the test string in any title tag.
Now, I do still have a few questions.
- What does the "type" parameter in RSpec... do? What's the real difference between a "feature" and a "request" spec?
- What would a view-based test for this detail look like? What are the advantages and disadvantages vs. a feature test?
- Are these feature tests, in fact, faster than the vanilla Rails controller tests?
For now, though, I'm satisfied, that these tests read naturally to me, are fast enough, and test what they look like they're testing.