Step by Step Guide to Feature Driven Development

Dec 16, 2015

Having seen an introduction to feature-driven development using RSpec and SimpleBDD (part 1 of this series), let's use it to construct a to-do list web app, step-by-step. This isn't going to be a complicated app, but it will illustrate the process.

Basic Setup

  • Create a new Rails application. Make sure you pass the -T flag to rails new to avoid generating the default testing infrastructure. The current version of Rails at the time of this writing is 4.2.3
  • Add the following to your Gemfile and run bundle install:
<span style="color: #0000ff;">group :test do
  gem 'capybara'
  gem 'database_cleaner'
  gem 'factory_girl_rails'
  gem 'faker'
  gem 'rspec'
  gem 'rspec-rails'
  gem 'shoulda-matchers'
  gem 'simple_bdd'
end</span>
  • Install RSpec:
<span style="color: #0000ff;">rails g rspec:install</span>
  • Configure your testing stack by changing your spec/rails_helper.rb file to the following:
<span style="color: #0000ff;">ENV['RAILS_ENV'] ||= 'test'
require 'spec_helper'
require File.expand_path('../../config/environment', __FILE__)
require 'rspec/rails'

require 'capybara/rspec'
require 'simple_bdd/rspec'

Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f }

ActiveRecord::Migration.maintain_test_schema!

RSpec.configure do |config|
  config.use_transactional_fixtures = true

  config.infer_spec_type_from_file_location!

  config.before(:suite) do
    DatabaseCleaner.strategy = :transaction
    DatabaseCleaner.clean_with(:truncation)

    begin
      DatabaseCleaner.start
      FactoryGirl.lint
    ensure
      DatabaseCleaner.clean
    end
  end

  config.around(:each) do |example|
    DatabaseCleaner.cleaning do
      example.run
    end
  end
end</span>

This will add all of the tools described above to your testing stack.

Finally, add the following to your config/application.rb to ensure that Rails generators generate the required testing infrastructure:

<span style="color: #0000ff;">config.generators do |g|
  g.template_engine :erb
  g.test_framework  :rspec, :fixture =&gt; true, :views =&gt; false
  g.integration_tool :rspec, :fixture =&gt; true, :views =&gt; true
  g.fixture_replacement :factory_girl, dir: 'spec/factories'
end</span>

Describing Features

In order to have a to-do list, users will need to be able to view all items, add new items, check off completed items, and delete items. A feature spec for the user management could live in spec/features/todo_list_spec.rb and look like this:

<span style="color: #0000ff;">require 'rails_helper'

feature 'Todo management' do
  scenario 'Adding an item to the list' do
    Given 'I am viewing the list'
    When 'I add a new item'
    Then 'I see the new item'
    And 'It is not completed'
  end

  scenario 'Viewing the list' do
    Given 'There is an item on the list'
    When 'I view the list'
    Then 'I see the item'
  end

  scenario 'Viewing an empty list' do
    Given 'There are no to-do list entries'
    When 'I view the list'
    Then 'I see that there are no entries'
  end

  scenario 'Completing an item' do
    Given 'There is an item on the list'
    When 'I view the list'
    When 'I complete the item'
    Then 'The item is completed'
  end
end</span>

A couple of things to note about this feature spec:

  • Each line describes an action that may take many steps to accomplish in the UI. The idea of feature specs is to describe features, not your UI, and you want to be able to make simple changes to the UI without breaking your feature specs.
  • Your feature specs all live under the spec/features/ directory. You may add subdirectories to this directory, and you may name the files whatever you wish, as long as they end in _spec.rb

Once this file is in place, run your specs. Since we've defined scenarios and steps, but not implemented the steps, you'll get four pending specs like this:

<span style="color: #0000ff;">  1) Todo management Adding an item to the list
     # i_am_viewing_the_list
     Failure/Error: Given 'I am viewing the list'
     SimpleBdd::StepNotImplemented:
       i_am_viewing_the_list</span>

The last line above contains the method you'll need to implement in order for that step to work, i_am_viewing_the_list. Let's implement that, within the feature block, but outside the scenario blocks:

<span style="color: #0000ff;">def i_am_viewing_the_list
  visit items_path
end</span>

This step function contains a Capybara command. visit instructs the virtual browser to go to the specified path. You can specify the path by relative URL ('/session/new') or using Rails URL helpers, as I've done here.

If you run this spec, you'll get an error for each step that begins with the 'I am viewing the list' step:

<span style="color: #0000ff;">1) Todo management Adding an item to the list
     Failure/Error: visit items_path
     NameError:
       undefined local variable or method `items_path' for #</span>

This tells us the next step in our development: the URL helper is missing, so add a route! Add the following to config/routes.rb:

<span style="color: #0000ff;">resources :items</span>

Re-running the spec will give you a new error for the tests that were failing before:

<span style="color: #0000ff;">1) Todo management Adding an item to the list
     Failure/Error: visit items_path
     ActionController::RoutingError:
       uninitialized constant ItemsController</span>

Now it's time to build a controller. While feature specs are intended to be very high-level, controllers and models are unit-tested, so we want to examine every case we can think of. Controller specs live under spec/controllers/ and are named by the controller they are testing. Here's spec/controllers/items_controller_spec.rb, which tests the :index action:

<span style="color: #0000ff;">require 'rails_helper'

describe ItemsController do
  describe '#index' do
    before do
      get :index
    end

    it 'renders the index' do
      expect(response).to render_template(:index)
    end
  end
end</span>

If you run this spec, it will fail for the same reason our feature spec failed: We don't have a UsersController yet. Let's add one with a new action in app/controllers/users_controller.rb:

<span style="color: #0000ff;">class ItemsController &lt; ApplicationController
  def index
  end
end</span>

Like before, RSpec is telling you what to do next: Make a view! The following goes in app/views/items/index.html.erb, and should look familiar to anyone who has done a Rails tutorial:

<span style="color: #0000ff;">&lt;div id="item-list"&gt;
  &lt;% @items.each do |item| %&gt;
    &lt;div id="item-&lt;%= item.id %&gt;" %&gt;
      &lt;%= item.text %&gt; &lt;%= item.completed ? "Completed" : "Not Completed" %&gt;
    &lt;/div&gt;
  &lt;% end %&gt;
&lt;/div&gt;</span>

Note that there are some CSS ids that don't seem necessary yet; they will assist with testing later. Running specs fail now because we haven't defined @items:

<span style="color: #0000ff;">1) Todo management Adding an item to the list
     Failure/Error: visit items_path
     ActionView::Template::Error:
       undefined method `each' for nil:NilClass</span>

So let's add that to the index action in ItemsController:

<span style="color: #0000ff;">class ItemsController &lt; ApplicationController
  def index
    @items = Item.all
  end
end</span>

That fails because we don't have an Item model:

<span style="color: #0000ff;">1) ItemsController#index renders the index
     Failure/Error: get :index
     NameError:
       uninitialized constant ItemsController::Item</span>

So let's make one using the Rails generators:

<span style="color: #0000ff;">rails g model item text:string completed:boolean</span>

This will generate a model spec. We can leave that pending now, as the Item model has no functionality. Run your migrations and run rspec again. Our first step in the feature specs now completes successfully, so we're back to pending!

<span style="color: #0000ff;">1) Todo management Adding an item to the list
     # i_add_a_new_item
     Failure/Error: When 'I add a new item'
     SimpleBdd::StepNotImplemented:
       i_add_a_new_item</span>

We need to implement the step where a new item is added. Describe how to do it by adding the following to spec/features/todo_list_spec.rb:

<span style="color: #0000ff;">let(:item_text) { Faker::Lorem.sentence }

  def i_add_a_new_item
    within('#new-item') do
      fill_in 'Text', with: item_text
      click_button 'Save'
    end
  end</span>

This step has a couple of Capybara commands that deserve some explanation:

  • within takes a CSS selector that lets you narrow down on the page where you want to perform an action or check for content. While at present the page doesn't contain much, it is good practice to narrow your searches down as much as possible.
  • fill_in and click_button do what's described on the tin; fill_in fills a text box with the given text, and click_button hits the submit button (or whichever button you specify).

Also, we're going to use a let for the actual item because we need to check later that the item appears on the page. lets are only evaluated once per feature.

This step will fail because we haven't added a CSS id #new-item yet:

<span style="color: #0000ff;">1) Todo management Adding an item to the list
     Failure/Error: within('#new-item') do
     Capybara::ElementNotFound:
       Unable to find css "#new-item"</span>

Let's go ahead and write the markup for the new item form, adding the following to the bottom of app/views/items/index.html.erb:

<span style="color: #0000ff;">&lt;div id="new-item"&gt;&lt;%= form_for :item do |f| %&gt;
  &lt;%= f.label :text %&gt;
  &lt;%= f.text_field :text %&gt;
  &lt;%= f.submit %&gt;
&lt;% end %&gt;&lt;/div&gt;</span>

This gives us a new error, once again indicating the next step in our development:

<span style="color: #0000ff;">1) Todo management Adding an item to the list
     Failure/Error: click_button 'Save'
     AbstractController::ActionNotFound:
       The action 'create' could not be found for ItemsController</span>

Let's write a spec for the create action before we write it, in spec/controllers/items_controller_spec.rb:

<span style="color: #0000ff;">
  describe '#create' do
    before do
      allow(Item).to receive(:create)
      post :create, { item: { text: Faker::Lorem.sentence } }
    end

    it 'creates an item' do
      expect(Item).to have_received(:create)
    end

    it 'redirects back to the index' do
      expect(response).to redirect_to items_path
    end
  end</span>

Then we can write the action. Since it's pretty simple, we'll do the whole thing in one shot, in app/controllers/items_controller.rb:

<span style="color: #0000ff;">
  def create
    Item.create(item_params)
    redirect_to items_path
  end

  private

  def item_params
    params.require(:item).permit(:text)
  end</span>

If we run rspec again, we'll see that the feature step we were working on passes, and we can continue to the next one:

<span style="color: #0000ff;">
  1) Todo management Adding an item to the list
     # i_see_the_item
     Failure/Error: Then 'I see the new item'
     SimpleBdd::StepNotImplemented:
       i_see_the_new_item</span>

Now, we can check that the item exists:

<span style="color: #0000ff;">
  def i_see_the_new_item
    within('#item-list') do
      expect(page).to have_content item_text
    end
  end</span>

Run your specs again, and you'll see that we've already done this, so the step passes. But the next step doesn't:

<span style="color: #0000ff;">
  1) Todo management Adding an item to the list
     # it_is_not_completed
     Failure/Error: And 'It is not completed'
     SimpleBdd::StepNotImplemented:
       it_is_not_completed</span>

We need some way to mark whether the item has been completed or not. Add a test as to whether the item is completed:

<span style="color: #0000ff;">
  def it_is_not_completed
    within('#item-list') do
      expect(page).to have_text "Not Completed"
    end
  end</span>

This will fail because the expected text was not found. Let's add it to the view in app/views/items/index.html.erb:

<span style="color: #0000ff;">&lt;div id="item-list"&gt;

  &lt;% @items.each do |item| %&gt;

</span>
<%= item.text %> <%= completed?(item) %></div> <% end %> </div>

Add the completed? helper in app/helpers/items_helper.rb:

<span style="color: #0000ff;">module ItemsHelper
  def completed?(item)
    if item.completed
      "Completed"
    else
      "Not Completed"
    end
  end
end</span>

You have your first passing feature spec! The next pending spec is a little different than the previous one, as it only requires that we set up the criteria for the test:

<span style="color: #0000ff;">
  1) Todo management Viewing the list
     # there_is_an_item_on_the_list
     Failure/Error: Given 'There is an item on the list'
     SimpleBdd::StepNotImplemented:
       there_is_an_item_on_the_list</span>

To make this one pass, use FactoryGirl to create an item. The rails generator created a factory when we generated our model, which we can find in spec/factories/item_factory.rb; the default factory is ok, but for our purposes it's better to add some randomness using Faker:

<span style="color: #0000ff;">FactoryGirl.define do
  factory :item do
    text { Faker::Lorem.sentence }
    completed false
  end
end</span>

So add this to the feature spec:

<span style="color: #0000ff;">  let(:item) { FactoryGirl.create(:item) }
  def there_is_an_item_on_the_list
    item
  end</span>

The next step is identical to another step we've already defined, just worded differently:

<span style="color: #0000ff;">1) Todo management Viewing the list
     # i_view_the_list
     Failure/Error: When 'I view the list'
     SimpleBdd::StepNotImplemented:
       i_view_the_list</span>

So, let's add a method alias:

<span style="color: #0000ff;">  def i_am_viewing_the_list
    visit items_path
  end
  alias_method :i_view_the_list, :i_am_viewing_the_list</span>

And we get another pending step:

<span style="color: #0000ff;">1) Todo management Viewing the list
     # i_see_the_item
     Failure/Error: Then 'I see the item'
     SimpleBdd::StepNotImplemented:
       i_see_the_item</span>

This is different from i_see_the_new_item, because we're looking for the item generated by FactoryGirl in the let(:item). To implement the step, we can do a similar check to the one for i_see_the_new_item, but because we have an item id, we can search in the actual line item:

<span style="color: #0000ff;">  def i_see_the_item
    within("#item-#{item.id}") do
      expect(page).to have_text item.text
    end
  end</span>

And we have another passing feature spec! Now we can finish up the third one:

<span style="color: #0000ff;">  1) Todo management Viewing an empty list
     # there_are_no_to_do_list_entries
     Failure/Error: Given 'There are no to-do list entries'
     SimpleBdd::StepNotImplemented:
       there_are_no_to_do_list_entries</span>

This is more of a destructive step than a constructive step:

<span style="color: #0000ff;">  def there_are_no_to_do_list_entries
    Item.destroy_all
  end</span>

The next step is looking for a blank state:

<span style="color: #0000ff;">1) Todo management Viewing an empty list
     # i_see_that_there_are_no_entries
     Failure/Error: Then 'I see that there are no entries'
     SimpleBdd::StepNotImplemented:
       i_see_that_there_are_no_entries</span>

So let's add the step:

<span style="color: #0000ff;">  def i_see_that_there_are_no_entries
    within('#item-list') do
      expect(page).to have_text "No items in list"
    end
  end</span>

This will fail because we haven't implemented the blank state yet:

<span style="color: #0000ff;">1) Todo management Viewing an empty list
     Failure/Error: expect(page).to have_text "No items in list"
       expected to find text "No items in list" in ""</span>

Let's add one in the view:

<span style="color: #0000ff;">&lt;div id="item-list"&gt;
  &lt;% if @items.size &gt; 0 %&gt;
    &lt;% @items.each do |item| %&gt;
      &lt;div id="item-&lt;%= item.id %&gt;"&gt;&lt;%= item.text %&gt; &lt;%= completed?(item) %&gt;&lt;/div&gt;
    &lt;% end %&gt;
 &lt;% else %&gt;
   No items in list
 &lt;% end %&gt;
&lt;/div&gt;
</span>

Only one feature spec left to go!

<span style="color: #0000ff;">1) Todo management Completing an item
     # i_complete_the_item
     Failure/Error: When 'I complete the item'
     SimpleBdd::StepNotImplemented:
       i_complete_the_item</span>

We need a step to complete the item. For now, let's just make it a link to click. In spec/features/todo_list_spec.rb:

<span style="color: #0000ff;">  def i_complete_the_item
    within("#item-#{item.id}") do
      click_link 'Mark Completed'
    end
  end</span>

And in app/views/items/index.html.erb:

<span style="color: #0000ff;">&lt;div id="item-&lt;%= item.id %&gt;"&gt;&lt;%= item.text %&gt; &lt;%= completed?(item) %&gt;
  &lt;%= link_to "Mark Completed", mark_completed_item_path(item) %&gt;&lt;/div&gt;</span>

You probably know why this is going to fail: We haven't defined the appropriate route. Let's do that:

<span style="color: #0000ff;">  resources :items do
    get :mark_completed, on: :member
  end</span>

And again, you know why the spec is going to fail: No action for mark_completed. Let's write a spec for that in spec/controllers/items_controller_spec.rb:

<span style="color: #0000ff;">  describe '#mark_completed' do
    let(:item) { double(:item) }
    let(:item_id) { rand(1000) }

    before do
      allow(Item).to receive(:find).with(item_id.to_s) { item }
      allow(item).to receive(:update_attributes) { true }
      get :mark_completed, id: item_id
    end

    it 'marks the item completed' do
      expect(item).to have_received(:update_attributes).with(completed: true)
    end

    it 'redirects back to the index' do
      expect(response).to redirect_to items_path
    end
  end</span>

You'll notice that this spec creates test doubles and stubs all of the action on the item model. The idea behind this is that your controller spec should only test what is happening in the controller. In your controller, stub what you expect the Item model to do, and in the spec on the Item model, you can make sure that what you stubbed in the controller is what is actually happening.

You may be wondering, if that's true, why haven't we written any model specs on Item yet? The answer is that there's no reason to; everything we're doing in Item is straight out of ActiveRecord, which is very well-tested. If, at a later point, we decide to add something to the Item model, like validations or callbacks, those will need to be tested.

Let's write an action in app/controllers/items_controller.rb to make the controller spec pass:

<span style="color: #0000ff;">  def mark_completed
    @item = Item.find(params[:id])
    @item.update_attributes(completed: true)
    redirect_to items_path
  end</span>

Finally, we have one last feature step to make pass:

<span style="color: #0000ff;">1) Todo management Completing an item
     # it_is_completed
     Failure/Error: Then 'The item is completed'
     SimpleBdd::StepNotImplemented:
       the_item_is_completed</span>

All we need to do is add the step:

<span style="color: #0000ff;">  def the_item_is_completed
    within("#item-#{item.id}") do
      expect(page).to have_text 'Completed'
      expect(page).to_not have_link 'Mark Completed'
    end
  end</span>

And make the view match the step:

<span style="color: #0000ff;">
&lt;div id="item-&lt;%= item.id %&gt;"&gt;&lt;%= item.text %&gt; &lt;%= completed?(item) %&gt;
&lt;% unless item.completed %&gt;
  &lt;%= link_to "Mark Completed", mark_completed_item_path(item) %&gt;
&lt;% end %&gt;&lt;/div&gt;
</span>

With that, you've completed your feature. The only remaining pending spec is the model. If you are done with the model, you can go ahead and remove the pending from that model and have a passing set of specs. But you're probably not done. You might want to be able to uncomplete completed items. You might want to delete items from the list. You might want to have a user authentication system and allow each user to have a separate list. You can build anything you'd normally build into a web application using this style.

Note that the application is completely unstyled; I haven't even told you to look at it in a web browser. When you do so, you'll probably be horrified at what it looks like, but it works exactly like you expect it to! Since you have good test coverage, you can start rearranging things on the page, adding CSS, and tweaking items without worrying too much about breaking things. If your specs start to fail, you can adapt your code and your tests to match the new desired behavior.

Stay tuned for part 3, which describes how to share step definitions among different features, a few best practices, and some comparison to other options!

To leverage content like this for your own business, contact Stride today.

You May Also Like

These Stories On Process

It's the third week in a row that your team has given the same update: "We are almost done with the 'previous orders' feature." You are frustrated with the lack of progress and the inability to shift to more important work—like that feature you wish the ... read more

Josh Seiden, the fourth speaker in Stride's Leadership through adversity speaker series, spoke about the value of focusing on outcomes, defined as: "measurable changes in behavior that drive business results."   read more

To start his talk, psychiatrist and psychoanalyst Dr. Kerry Sulkowicz admitted to being at a bit of a loss when asked about best practices for leading through a pandemic. "The honest answer is I don't know," he stated, "because none of us has lived ... read more

Taking direct aim at the narrative of the "all in" entrepreneur who takes extreme risks and depletes their bank account before ultimately succeeding, noted NYC VC Charlie O'Donnell started his Stride Consulting “Leading through Adversity” talk on May ... read more

Get Email Updates