Modern Cucumber and Rails: No More Training Wheels

Last month, cucumber-rails 1.1 was released. This release removed web_steps.rb, a collection of step definitions for interacting with a web app.

For months, web_steps.rb contained a warning of its negative effects on feature maintenance. Like most developers, I ignored the warning. During a recent upgrade of an existing Rails app, I realized it was now gone. Instead of copying and pasting it from an older app or using the newly created cucumber-rails-training-wheels gem, I decided to accept the challenge and refactor its steps out of the app’s existing features.

After the refactor, the features read much better. They were simpler, less verbose, and felt more maintainable. I also cleaned up my factory_girl usage, which was causing issues similar to web_steps.rb. Here’s a short overview of the main refactorings.

Removing Domain Leakage

The first step in writing a scenario is establishing its context. This usually involves creating some domain objects. For setup, I use factory_girl. factory_girl includes step definitions for calling your app’s custom factories from scenarios.

The following scenario is from a simple CMS. The domain model consists of a single Page class with a page_type attribute. factory_girl factories exist for creating various types of pages e.g., a home page, a category page, a topic page, etc. A hierarchy of pages can be created by associating a page with a parent page. This scenario is for creating a subtopic page.

factory_girl generated the three “And the following <factory-name> exists:” steps from the app’s existing factories. Each of these steps includes a table with the names and values of attributes to be passed to the corresponding factory.

The table in both the category and topic step includes an association. For example, in the topic step factory_girl will set the topic’s parent association to the existing page named “Mind”, or if that page does not exist, to a new page with that name.

There are two problems with this scenario:

Dependent on the domain model. This scenario contains the names of classes, their attributes, and even their associations. This is brittle and could break due to domain changes as minor as renaming a database column.

Includes too much detail. This scenario is about creating a subtopic, the actual test data should not matter. The fact that the name of the topic is “Stress” is distracting and irrelevant. The home and category pages are also unnecessary; only a topic page should be required to create a subtopic page. Factories should handle any required associations. These nonessential elements are known as incidental details.

Let’s refactor to something simpler.

This refactored version replaces the former setup with a single step, also provided by factory_girl.factory_girl generates a “Given a <factory-name> exists” for each of your factories. This simpler step definition excludes the ascii table filled with implementation details, eliminating distracting test data, and making the scenario more readable overall.

Strive to use factory_girl‘s simpler step definitions to create more maintainable scenarios. Avoid writing scenarios with the more verbose table based steps by eliminating incidental details. Don’t hesitate to create custom steps if factory_girl‘s steps are too low level for a scenario.

Thinking Declaratively

Here’s a scenario from the same simple CMS project using various action steps provided by web_steps.rb.

There are two problems with this scenario:

Dependent on the UI. Changes to the UI could result in corresponding changes to this feature. Scenarios written in this step-by-step style are known as imperative scenarios.

Contains too many incidental details. A stakeholder doesn’t care about the test data, they just want to verify that you can create a slide.

Let’s refactor and fix these two issues.

A single custom step has eliminated our UI dependency and removed insignificant data. This scenario is much more readable and as a result, more likely to be read.

The important point is to not be lazy and lean on web_steps.rb. Instead, write in a declarative style by creating custom steps that focus on “what” should happen, not “how” it should happen.

Simpler Verification

web_steps.rb also provides several step definitions for verifying page content and form field values.

The omitted verification step of the previous “Create a slide” scenario uses these steps to verify a success message and form field values:

This scenario implies that after creating a slide you’re shown a success message and redirected to the new slide’s edit page. Again, this creates a dependency on the UI and distracts with its “interesting data”.

Let’s refactor by first moving the success message verification into the “And I create a slide” step. Then we can introduce a new higher-level step to summarize the redirection and expected form field values.

Make the Effort

The removal of web_steps.rb was a controversial move but also a good one. Like most developers, I ignored the warnings in web_steps.rb and my scenarios suffered from unexpected failures and UI dependencies. By eliminating web_steps.rb, you’re forced to think declaratively and write your features at a much-higher level. Take the same approach during setup when using factory_girl. I abused factory_girl‘s step definitions and my features soon became lost in ascii tables.

You will have to write more code, but the improved readability and maintenance make it worth the effort.

Feedback

Comments: 18

Rob Pak

Now that you have removed references to your models from your features, that does not mean that your features are less brittle. The model references were moved into your step definitions, right? I don’t think that this will make your features less brittle, they will be just as brittle as they were before. A simple database column rename can still break your step definitions…

Jared Carroll

That’s true Rob, I removed the implementation details from the plain text feature files to the step definitions. This means that the step definitions are still susceptible to domain/UI changes. You can’t complete avoid the dependency but you can at least remove it from your feature files where it hurts readability.

Tommi

Jared Carroll

Once you’re at the step definitions it’s Ruby code. You still need to make these as readable as you can with Ruby. But the step definitions aren’t meant to be frequently read and used as documentation by stakeholders, so I’d say it’s more important that the features are easier to read than the step definitions.

Tommi

You are right, for stakeholders it is better to read the scenarios the way you have refactored in examples. However, I’m concerned that the readability goes so down on the step definition level that it becomes a burden to maintain those.

Rudy Jahchan

I completely agree that these are much cleaner and terser tests. I have concern and one recommendation though.

The concern is taking this too far so that you effectively end up with fat steps that are not reusable. We should keep it mind.

And my recommendation is to have a follow up blog post explaining how to use instance variables or module mixins to allow steps to say “A topic exists” and the following steps to know about that topic … and the pitfalls that may be introduced when they are in the wrong order!

John Nieri

Thank you very much for this post. I keep coming back to it either intentionally or through google as I try to make cucumber work for me. I have a specific question about your last “Create a Slide” scenario. I came up with a similar structure for creating a certain resource in my application and was happy to see the same thing in yours (small victories).

However, for the step definitions, I’m stumped for a good way to deal with the jump between: “and I create a new slide” and ” I should be able to edit it”. I’m using capybara so for a WHEN step, I go through the process required to create one as a user would and then….. how to you confidently identify that post you just created? I currently do this:

@post = Post.where(:slug => manually generated slug).first

which introduces dependencies that I don’t want as well as making it more brittle. How do you get the reference to the post for the next step of making sure you have access to it?

Jared Carroll

To get the most recently created Slide in the “Then I should be able to edit it” step definition, I do “Slide.last”. This returns the last (newest) row in the slides table.

In the “Then I should be able to edit it” step definition, instead of verifying HTTP response codes, I like to verify that the form fields contain the values of the most recently created slide. You could also go a step further and fill in new values, submit the form, and verify the update; but I think that’s more appropriate in a separate “Edit a slide” scenario.

I typically wouldn’t verify the edit page’s url (page.current_path.should == “/admin/slides/#{Slide.last.id}/edit”) but I don’t see any problem with that.

Jared Carroll

It was a custom step because I wasn’t using paths.rb. Although, implementing custom steps that essentially just do a “visit(path)” starts to get pretty ridiculous. paths.rb only relied on 1 step in web_steps.rb (“I go to (.+)”). I usually end up implementing something very similar to paths.rb in order to avoid writing a ton of 1 line steps like “I go to the home page”.