BDD on Rails with Minitest, Part 2: Implementing a Feature

· 3 Nov 2014 · Semaphore Engineering Blog

BDD on Rails with Minitest tutorial, part 2

Chris Kottom, guest author on Semaphore blog

This is a guest blog post from Chris Kottom, a longtime Ruby and Rails developer, freelancer, proud dad, and maker of the best chili con carne to be found in Prague. He's currently working day and night on the upcoming e-book The Minitest Cookbook.


A lot of Rubyists and Rails developers use a behavior-driven (BDD) workflow to give structure to feature development. Adoption of BDD using tools like RSpec and Cucumber has been fueled in part by the availability of high-quality books and blog posts on the subject, but RSpec is certainly not the only game in town. Many programmers have begun looking for alternative approaches, for example, based on the Minitest unit testing library which is included in Ruby distributions and provides similar functionality in a more compact implementation.

In this post, we'll walk through a complete feature implementation and show how to build realistic functionality into our application using our tests to lead us through the process. If you haven't read the first part of the series where we described how to assemble and get started using a lean and mean Rails testing stack based on Minitest, you should consult that post first since the examples in this post pick up where we left off.

Feature: Display a List of To-Do Items

In part one, we implemented an empty list of to-do items. Now we're ready to display a full list of items from the database. To do this, we'll update the tests to verify that all to-do items are shown in the list and that the name and description of each item is displayed. The updated feature to look like this:

require "test_helper"

feature "To Do List" do
  scenario "displays a list of to-do items" do
    visit root_path
    page.must_have_css("#items")
    within("#items") do
      Item.find_each do |item|
        selector = "#item-#{ item.id }"
        page.must_have_css(selector)
        within(selector) do
          page.must_have_content item.name
          page.must_have_content item.description
        end
      end
    end
  end
end

Once we've done that, we re-run our suite and see that our feature spec is once again failing.

ItemsController::GET :index
  test_0001_renders items/index                                  PASS (0.06s)
  test_0002_responds with success                                PASS (0.01s)

To Do List Feature Test
  test_0001_displays a list of to-do items                       ERROR (0.04s)
NameError:         NameError: uninitialized constant Item
        test/features/to_do_list_test.rb:8:in `block (3 levels) in <top (required)>'
        test/features/to_do_list_test.rb:7:in `block (2 levels) in <top (required)>'
        test/features/to_do_list_test.rb:8:in `block (3 levels) in <top (required)>'
        test/features/to_do_list_test.rb:7:in `block (2 levels) in <top (required)>'

Finished in 0.12654s
3 tests, 3 assertions, 0 failures, 1 errors, 0 skips

In strict BDD, we'd want to step through this very slowly and one step at a time, but to speed things along and keep from having to write separate migrations for each and every attribute separately, we'll collapse what we currently know about to-do list items and their attributes into a single failing test case.

require "test_helper"

describe "Item" do
  before do
    @item = Item.new(name: "Write Minitest-BDD post",
                     description: "Show Rails and Capybara example")
  end

  it "has a name attribute" do
    @item.must_respond_to :name
  end

  it "has a description attribute" do
    @item.must_respond_to :description
  end
end

The test syntax you see here is provided by Minitest::Spec which provides a declarative API for describing the expected state of objects. In this example, I can call #must_respond_to on any object with a Symbol or String argument to test whether or not the object responds to the named method. The Minitest::Spec DSL is very compact by itself, but it's trivial to extend t with your own custom expectations that will improve the readability and expressiveness of your tests.

These new model tests will fail for the same reason as the controller and feature tests: there's no Item model yet defined.

ItemsController::GET :index
  test_0001_renders items/index                                  PASS (0.06s)
  test_0002_responds with success                                PASS (0.01s)

To Do List Feature Test
  test_0001_displays a list of to-do items                       ERROR (0.04s)
NameError:         NameError: uninitialized constant Item
        test/features/to_do_list_test.rb:8:in `block (3 levels) in <top (required)>'
        test/features/to_do_list_test.rb:7:in `block (2 levels) in <top (required)>'
        test/features/to_do_list_test.rb:8:in `block (3 levels) in <top (required)>'
        test/features/to_do_list_test.rb:7:in `block (2 levels) in <top (required)>'

Item
  test_0002_has a description attribute                          ERROR (0.00s)
NameError:         NameError: uninitialized constant Item
        test/models/item_test.rb:5:in `block (2 levels) in <top (required)>'
        test/models/item_test.rb:5:in `block (2 levels) in <top (required)>'

  test_0001_has a name attribute                                 ERROR (0.00s)
NameError:         NameError: uninitialized constant Item
        test/models/item_test.rb:5:in `block (2 levels) in <top (required)>'
        test/models/item_test.rb:5:in `block (2 levels) in <top (required)>'


Finished in 0.13013s
5 tests, 3 assertions, 0 failures, 3 errors, 0 skips

All of the current errors will be resolved by generating a new Item model with :name and :description attributes and running migrations. Our three errors collapse into one failure.

Item
  test_0001_has a name attribute                                 PASS (0.01s)
  test_0002_has a description attribute                          PASS (0.00s)

ItemsController::GET :index
  test_0001_renders items/index                                  PASS (0.30s)
  test_0002_responds with success                                PASS (0.01s)

To Do List Feature Test
  test_0001_displays a list of to-do items                       FAIL (0.05s)
Minitest::Assertion:         expected to find #item-298486374.
        test/features/to_do_list_test.rb:10:in `block (4 levels) in <top (required)>'
        test/features/to_do_list_test.rb:8:in `block (3 levels) in <top (required)>'
        test/features/to_do_list_test.rb:7:in `block (2 levels) in <top (required)>'

Finished in 0.38305s
5 tests, 6 assertions, 1 failures, 0 errors, 0 skips

We now need to modify the view to display our list of items, and just to speed things up here, we'll break with strict BDD and once again combine individual changes to the view to include the name and description of the item as well as the id selector that we'll need. In reality, our view might end up looking like this:

<%= content_tag :div, class: 'items' do %>
  <% @items.each do |item| %>
    <%= content_tag :p, id: "item-#{ item.id }", class: 'item' do %>
    <%= content_tag :b, item.name %>
      <br>
      <%= item.description %>
    <% end -%>
  <% end -%>
<% end -%>

In the end, we end up with errors across all tests that touch the view.

Item
  test_0002_has a description attribute                          PASS (0.00s)
  test_0001_has a name attribute                                 PASS (0.00s)

ItemsController::GET :index
  test_0001_renders items/index                                  ERROR (0.11s)
ActionView::Template::Error:         ActionView::Template::Error: undefined method `each' for nil:NilClass
        app/views/items/index.html.erb:2:in `_app_views_items_index_html_erb___267186296561738095_69977433372580'
        test/controllers/items_controller_test.rb:6:in `block (3 levels) in <top (required)>'
        app/views/items/index.html.erb:2:in `_app_views_items_index_html_erb___267186296561738095_69977433372580'
        test/controllers/items_controller_test.rb:6:in `block (3 levels) in <top (required)>'

  test_0002_responds with success                                ERROR (0.00s)
ActionView::Template::Error:         ActionView::Template::Error: undefined method `each' for nil:NilClass
        app/views/items/index.html.erb:2:in `_app_views_items_index_html_erb___267186296561738095_69977433372580'
        test/controllers/items_controller_test.rb:6:in `block (3 levels) in <top (required)>'
        app/views/items/index.html.erb:2:in `_app_views_items_index_html_erb___267186296561738095_69977433372580'
        test/controllers/items_controller_test.rb:6:in `block (3 levels) in <top (required)>'


To Do List Feature Test
  test_0001_displays a list of to-do items                       ERROR (0.01s)
ActionView::Template::Error:         ActionView::Template::Error: undefined method `each' for nil:NilClass
        app/views/items/index.html.erb:2:in `_app_views_items_index_html_erb___267186296561738095_69977433372580'
        test/features/to_do_list_test.rb:5:in `block (2 levels) in <top (required)>'
        app/views/items/index.html.erb:2:in `_app_views_items_index_html_erb___267186296561738095_69977433372580'
        test/features/to_do_list_test.rb:5:in `block (2 levels) in <top (required)>'

Finished in 0.14093s
5 tests, 2 assertions, 0 failures, 3 errors, 0 skips

The cause of these failures is the same in all cases: the collection of to-do items is not yet being set by the controller. To resolve this, we first write a failing test on the controller that checks that the collection is assigned and has the right values.

it "fetches and assigns a list of to-do items" do
  assigns(:items).wont_be_nil

  item_ids = assigns(:items).map(&:id).sort
  item_ids.must_equal Item.pluck(:id).sort
end

Here again, we see more of the Minitest::Spec DSL in action. Minitest provides negative expectations like #wont_be_nil to check for the absence of a given condition - in this case, that the object under test is non-nil.

Unlike RSpec, Minitest doesn't provide a way of testing whether two Arrays contain the same elements, but this is easy enough to implement in the case of ActiveRecord relations simply by comparing two sorted arrays of IDs as shown in the example.

Including this test adds one more failure to our suite:

To Do List Feature Test
  test_0001_displays a list of to-do items                       ERROR (0.14s)
ActionView::Template::Error:         ActionView::Template::Error: undefined method `each' for nil:NilClass
        app/views/items/index.html.erb:2:in `_app_views_items_index_html_erb___1343446998779429432_69831990148440'
        test/features/to_do_list_test.rb:5:in `block (2 levels) in <top (required)>'
        app/views/items/index.html.erb:2:in `_app_views_items_index_html_erb___1343446998779429432_69831990148440'
        test/features/to_do_list_test.rb:5:in `block (2 levels) in <top (required)>'

Item
  test_0001_has a name attribute                                 PASS (0.00s)
  test_0002_has a description attribute                          PASS (0.00s)

ItemsController::GET :index
  test_0001_renders items/index                                  ERROR (0.00s)
ActionView::Template::Error:         ActionView::Template::Error: undefined method `each' for nil:NilClass
        app/views/items/index.html.erb:2:in `_app_views_items_index_html_erb___1343446998779429432_69831990148440'
        test/controllers/items_controller_test.rb:6:in `block (3 levels) in <top (required)>'
        app/views/items/index.html.erb:2:in `_app_views_items_index_html_erb___1343446998779429432_69831990148440'
        test/controllers/items_controller_test.rb:6:in `block (3 levels) in <top (required)>'

  test_0002_responds with success                                ERROR (0.00s)
ActionView::Template::Error:         ActionView::Template::Error: undefined method `each' for nil:NilClass
        app/views/items/index.html.erb:2:in `_app_views_items_index_html_erb___1343446998779429432_69831990148440'
        test/controllers/items_controller_test.rb:6:in `block (3 levels) in <top (required)>'
        app/views/items/index.html.erb:2:in `_app_views_items_index_html_erb___1343446998779429432_69831990148440'
        test/controllers/items_controller_test.rb:6:in `block (3 levels) in <top (required)>'

  test_0003_fetches and assigns a list of to-do items            ERROR (0.00s)
ActionView::Template::Error:         ActionView::Template::Error: undefined method `each' for nil:NilClass
        app/views/items/index.html.erb:2:in `_app_views_items_index_html_erb___1343446998779429432_69831990148440'
        test/controllers/items_controller_test.rb:6:in `block (3 levels) in <top (required)>'
        app/views/items/index.html.erb:2:in `_app_views_items_index_html_erb___1343446998779429432_69831990148440'
        test/controllers/items_controller_test.rb:6:in `block (3 levels) in <top (required)>'

Finished in 0.15893s
6 tests, 2 assertions, 0 failures, 4 errors, 0 skips

And now we only need to update the controller action to make the tests pass:

class ItemsController < ApplicationController
  def index
    @items = Item.all
  end
end

And drumroll please...

Item
  test_0001_has a name attribute                                 PASS (0.00s)
  test_0002_has a description attribute                          PASS (0.00s)

To Do List Feature Test
  test_0001_displays a list of to-do items                       PASS (0.15s)

ItemsController::GET :index
  test_0001_renders items/index                                  PASS (0.00s)
  test_0002_responds with success                                PASS (0.00s)
  test_0003_fetches and assigns a list of to-do items            PASS (0.00s)

Finished in 0.16973s
6 tests, 13 assertions, 0 failures, 0 errors, 0 skips

This simple example only scratches the surface of what Minitest can do, of course, but it shows off its status as a viable alternative to RSpec. Minitest has a strong and growing ecosystem surrounding it that makes further customization and extension possible to suit your workflow - whether that's BDD or any other. If you're looking for a slim alternative to RSpec, it might be worth checking out.

For more information about Minitest and the related gems used in this post, check out some of the following resources:

Newsletter

comments powered by Disqus
Newsletter

Occasional lightweight product and blog updates. Unsubscribe at any time.

© 2009-2017 Rendered Text. All rights reserved. Terms of Service, Privacy policy, Security.