26 Mar 2020 · Software Engineering

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

    10 min read
    Contents

    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 '
            test/features/to_do_list_test.rb:7:in `block (2 levels) in '
            test/features/to_do_list_test.rb:8:in `block (3 levels) in '
            test/features/to_do_list_test.rb:7:in `block (2 levels) in '
    
    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 '
            test/features/to_do_list_test.rb:7:in `block (2 levels) in '
            test/features/to_do_list_test.rb:8:in `block (3 levels) in '
            test/features/to_do_list_test.rb:7:in `block (2 levels) in '
    
    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 '
            test/models/item_test.rb:5:in `block (2 levels) in '
    
      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 '
            test/models/item_test.rb:5:in `block (2 levels) in '
    
    
    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 '
            test/features/to_do_list_test.rb:8:in `block (3 levels) in '
            test/features/to_do_list_test.rb:7:in `block (2 levels) in '
    
    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:

    erb
    <%%= 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 %>
          
          <%%= 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 '
            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 '
    
      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 '
            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 '
    
    
    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 '
            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 '
    
    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 '
            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 '
    
    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 '
            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 '
    
      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 '
            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 '
    
      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 '
            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 '
    
    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:

    * Minitest – GitHub, RDoc
    * Capybara – GitHub, RDoc
    * minitest-rails – GitHub
    * minitest-rails-capybara – GitHub

    Leave a Reply

    Your email address will not be published. Required fields are marked *

    Avatar
    Writen by:
    Marko Anastasov is a software engineer, author, and co-founder of Semaphore. He worked on building and scaling Semaphore from an idea to a cloud-based platform used by some of the world’s engineering teams.