BDD in Ruby on Rails with RSpec and Capybara
Why controller specs often re-test what Rails already guarantees, and how feature tests with RSpec and Capybara let you test what the user actually sees.
Most of my posts have been about testing in Ruby on Rails, usually unit and functional tests. This time I'll cover something a bit more complex — a topic that gave me existential crises while testing, one you may know absolutely nothing about, but that I promise you'll want to keep digging into once you finish reading.
And yes… it's time to talk about BDD.
Introduction
Behavior-driven development is a newer take on TDD that was born years ago to write tests based on your application's features.
Some say BDD replaces TDD, others say BDD is the same as TDD, others that BDD is something completely different from TDD… [that's a lot of Ts and Ds, isn't it? ( ಠ_ಠ )]
Either way, BDD has one clear, specific point: tests should read as close to natural human language as possible. I'll show an example later of what I mean by this.
In Ruby on Rails, TDD usually means unit tests for models and functional tests for controllers. The trap is that you often end up testing something painfully obvious — or worse, testing behavior the system already provides by default.
BDD follows the same tests-first steps as TDD, but it builds tests from another angle: test what you see. The idea behind BDD is to test the things you actually see in the application — the end results after performing certain actions — verifying that the app does exactly what it's supposed to do, instead of fooling yourself with simple unit tests that, as I said before, often do little more than re-test what the system already does out of the box.
BDD everywhere…
When doing TDD in RoR, I mostly write unit tests for models and functional tests for controllers. In my controller tests, one of the most common specs is:
expect(response).to render_template 'template_name'After some time working with TDD, a question crept in that made me doubt my own existence: why test behavior that Rails already provides by default?
What I mean is that a controller already renders a view on its own. In a PostsController, an INDEX action renders the index view, a SHOW action renders the show view — Rails guarantees each action renders its corresponding view unless we specify otherwise. So if we haven't customized the rendering at all… why test it?
Why test something the system already does by default? Is it really necessary to write tests for these cases? Why? What for?…
Searching the vast internet for an answer, I ran into a question on StackOverflow similar to mine — well, not exactly similar; I remember it asked precisely how one should test view rendering.
Going through the answers, I found one that settled my question. It started by explaining how to write those specs in RSpec using 'render_template' and so on, but it closed with a recommendation: this wise gentleman explained that there's no need to test view rendering at all. What you should do instead is write feature tests — high-level tests — that verify the application's final behavior: use BDD to navigate the app and check that the data shows up correctly.
Let's take a simple example. I have a PostsController with INDEX and SHOW actions. With plain TDD I'd normally write:
# INDEX action
it 'show the correct view' do
expect(response).to render_template 'index'
end
# SHOW action
it 'show the correct view' do
expect(response).to render_template 'show'
endand with those two specs, one per method, we'd call it a day.
But with BDD we don't test that an action renders its corresponding view — we should test the end result, what we actually want the application to do. Let me show you with a feature spec using RSpec and Capybara for our SHOW action:
feature 'Show the post selected' do
scenario 'user want to see the first post' do
visit posts_path
click_link 'Its my first posts'
expect(page).to have_content 'Its my first post!' # post title shown in the web app
expect(page).to have_content 'Welcome to my first post guys' # post body
end
endIn Capybara, a
featureis equivalent to adescribeand ascenarioto anit.
Let's walk through the code:
- Line 3: the user visits the URL that lists all posts.
- Line 4: the user clicks the link of the first post, named 'Its my first post'.
- Line 5: we tell our test that the page should contain the first post's title, 'Its my first posts' — which could be an h1, an h2 or even a p (paragraph).
- Line 6: we tell our test that the page should also contain the post's body, in this case 'Welcome to my first…'.
What we're testing above is that all the post's data shows up when someone hits the corresponding URL. Capybara makes this possible by letting us drive the application through external interfaces — in our case, a web page. In other words: we can simulate the real behavior a user would have with our application.
The first thing to notice is that we no longer need to test that the correct view is rendered — our feature test covers that and much more. It verifies that:
- The URL for my first post is generated correctly.
- The URL actually works.
- The URL actually takes me to the post I want.
- The page I land on after clicking the first post's link actually shows its title and body.
See it? We have a much more powerful test — one that, so to speak, has tests embedded inside it.
Let's look at our feature test a bit more:
feature 'Show the post selected' do
scenario 'user want to see the first post' do
visit posts_path
click_link 'Its my first posts'
expect(page).to have_content 'Its my first post!'
expect(page).to have_content 'Welcome to my first post guys'
end
endNotice how verbose the test is? The syntax is very, very close to human language! And that's another key point of BDD: making your tests easy to read and understand — so easy that even a non-developer can follow them.
Wondering why you'd want your tests to be easy to read and understand? Because it helps eliminate some of the problems TDD can bring… how? Being able to read your tests as plain sentences changes how you think about them; the point is that if you understand them, you'll be able to write better, more complete tests.
There's still a lot left to cover — acceptance tests, visual tests, generating reports from your tests, why choose between Cucumber and Capybara, and much more. I'll stop here to keep the post from getting longer than it already is. Don't worry, I plan to write another post covering the remaining topics.
Test your features, not the ones inherent to the system…
Finally
BDD makes you focus on user behavior — which is, of course, based on your application's features — while writing tests that are easier to read and understand, all with the goal of having better tests.
I'm not saying we should stop using TDD; I'm saying we should stop testing things our favorite framework already does by default, and start writing more complete, more powerful tests! ʕ•ܫ•ʔ
Thanks for reading! If you liked the post, share it with anyone who might find it useful — after all, that's my goal: trying to help people.
Finally, here are some of my other posts about testing in Ruby on Rails (◕‿◕), in Spanish: