Readable feature tests with RSpec
This was originally posted to the FutureLean blog. See archived version.
Testing is an integral part of building and maintaining a large platform. When we build new parts of the FutureLearn platform we write automated feature tests to document how the new feature should work and to let us know when it doesn’t.
Cucumber Love-hate
Cucumber is a commonly used tool for writing feature specs, and seemed like the obvious choice to use when we started the project. It let us write high level behaviour driven tests from the user’s perspective:
Feature: Enrolment
Scenario: Enrolling in a course
Given there is a course
And I am logged in as a learner
When I enrol on a course
Then the course should appear in 'my courses'
We liked Cucumber because it meant the tests were very easy to create from our user stories, and once written, easy to read. However, there were a few downsides to using Cucumber. Firstly, we were already using RSpec for the rest of the project, so having Cucumber meant another test framework dependency that we had to maintain. Secondly, there was a cognitive overhead of switching between them because of the different DSLs and test runners. Finally, we particularly didn’t like Cucumber’s use of regular expressions, because they made it harder to follow what code is being executed when tests are run as compared to standard method invocation in Ruby.
Writing better RSpec features
So, how could we stop using Cucumber but keep the readable tests that we liked?
We had already started using RSpec features instead of Cucumber, and often they looked like this:
feature 'Enrolment' do
scenario 'Enrolling in a course' do
course = FactoryGirl.create(:course)
learner = FactoryGirl.create(:learner)
login_as learner
visit course_path(course)
find('.join').click
expect(page).to have_content('Thanks for joining!')
visit '/'
expect(page).to have_main_header('My Courses')
expect(page).to have_content(course.full_title)
end
end
These had a tendency to be quite long, making it difficult to understand what they were testing. There is also no clear separation between the Arrange, Act, Assert parts of the test (known as ‘Given’, ‘When’ and ‘Then’ in the language of Cucumber). We tried adding comments to the steps in the code, but found that these suffered the same fate as comments often do in application code: after time they get out of sync with the actual code.
Typically, a method this long elsewhere in our application code would have been a red flag, which we’d normally refactor through the therapeutic act of method extraction. So why not do just that? Let’s extract those lines and name each method in the same style as a Cucumber step:
feature 'Enrolment' do
scenario 'Enrolling in a course' do
given_there_is_a_course
and_i_am_logged_in_as_a_learner
when_i_enrol_on_a_course
then_the_course_should_appear_in_my_courses
end
def given_there_is_a_course
@course = FactoryGirl.create(:course)
end
def and_i_am_logged_in_as_a_learner
@learner = FactoryGirl.create(:learner)
login_as @learner
end
def when_i_enrol_on_a_course
visit course_path(@course)
find('.join').click
expect(page).to have_content('Thanks for joining!')
end
def then_the_course_should_appear_in_my_courses
visit '/'
expect(page).to have_main_header('My Courses')
expect(page).to have_content(@course.full_title)
end
end
What we found
We’ve since removed all of our Cucumber features and converted most of them to these new style RSpec feature specs. They retain the benefit of readability that Cucumber previously provided, yet are much easier to write and maintain.
We made a conscious decision not to optimise for code-reuse of our extracted methods between different feature files as we were worried that it would make the tests harder to follow. We have found that some code reuse tends to happen naturally when writing multiple scenarios around a single feature.
You can read more by me, follow me on Mastodon or subscribe.

I've just published my book - Build a Database Server. Learn how real databases like PostgreSQL and MySQL work by building your own database from scratch.
Find out more and see a preview.