24 Nov 2021 · Semaphore News

    BDD on Rails with Minitest, Part 1: Up and Running

    8 min read
    Contents

    BDD on Rails with Minitest tutorial

    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.


    Behavior-driven development (BDD) has gained mindshare within the Ruby and Rails communities in no small part because of a full-featured set of tools that enables development guided by tests at many different levels of the application. The early leaders in the field have been RSpec and Cucumber which have each attracted millions of users and dozens if not hundreds of contributors. But a growing number of Rubyists have started to build and use testing stacks based on Minitest which provides a comparable unit testing feature set in a simplified, slimmed-down package.

    In this two-part series, I’ll show you how to implement a BDD workflow based on Minitest and hopefully introduce you to an alternative means for getting the same behavior-driven goodness into your application in the process. In this post, you’ll see how to set up your testing stack and run through a quick iteration to verify that everything is configured and working properly.

    Setting Up

    As an example, we’ll work on a simple Rails application that lists to-do items consisting of a name and a description initially and build that BDD-style using Minitest and other supporting tools. Before writing the first test, we need to install and configure our testing stack. In this case, we’re going with a combination of gems that will provide an experience that should be familiar to regular RSpec users:

    * minitest for unit testing
    * capybara for acceptance testing
    * minitest-rails-capybara to integrate both testing libraries with Rails
    * minitest-reporters for test output customization including colorizing

    To install the required gems, we need to add the following lines to the Gemfile.

    group :test do
      gem 'minitest-rails-capybara'
      gem 'minitest-reporters'
    end
    

    Minitest supports two different methods for writing tests: a default assert-style syntax which resembles class Test::Unit and a spec-style syntax that more closely resembles RSpec. I personally prefer the spec-style syntax, so for this example, we’ll be writing all the tests as specs. To force the generators to produce spec-style test cases, we need to tell them to do so by adding the following block to config/application.rb:

    # Use Minitest for generating new tests.
    config.generators do |g|
      g.test_framework :minitest, spec: true
    end
    

    Next, we’ll update our test_helper.rb by running the minitest-rails generator: rails generate minitest:install. The new version of the test helper requires minitest-rails as a basis for all tests. We’ll modify that a bit further so that the final version also requires rails-minitest-capybara and minitest-reporters and configures the reporters. The finished product should look something like this:

    ENV["RAILS_ENV"] = "test"
    require File.expand_path("../../config/environment", __FILE__)
    require "rails/test_help"
    require "minitest/rails"
    require "minitest/rails/capybara"
    
    require "minitest/reporters"
    Minitest::Reporters.use!(
      Minitest::Reporters::SpecReporter.new,
      ENV,
      Minitest.backtrace_filter
    )
    class ActiveSupport::TestCase
      ActiveRecord::Migration.check_pending!
      fixtures :all
    end
    

    A Failing Feature

    The most basic feature of our to-do list will be a page that displays a list containing our to-do items, so we’ll start by generating a new Capybara feature using the provided generator: rails generate minitest:feature ToDoList. That initializes a boilerplate test in test/features that we can then update to suit our needs. We’ll begin by writing the most basic possible test for this application with minitest-rails-capybara providing a nice bridge between two worlds – packaging the Capybara DSL as Minitest-style assertions and expectations.

    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")
      end
    end
    

    When I run my test suite, it bombs. Predictably.

    To Do List Feature Test
      test_0001_displays a list of to-do items                       ERROR (0.00s)
    NameError:         NameError: undefined local variable or method `root_path' for #<#:0x007f83c4153930>
            test/features/to_do_list_test.rb:5:in `block (2 levels) in '
            test/features/to_do_list_test.rb:5:in `block (2 levels) in '
    
    Finished in 0.00508s
    1 tests, 0 assertions, 0 failures, 1 errors, 0 skips
    

    This is exactly what we hope to see, of course. Our workflow dictates that we first establish the desired behavior, see that it fails, and then write the code to make it pass. We’re now ready to begin stepping our way through the implementation using the tests as a feedback mechanism.

    The Red-Green Dance

    The initial error occurs because the application has no routes defined yet, so we’ll need to add a root path pointing to a hypothetical ItemsController to config/routes.rb:

    Rails.application.routes.draw do
      root to: 'items#index'
    end
    

    When we re-run the tests, the error that occurs has changed.

    erb
    To Do List Feature Test
      test_0001_displays a list of to-do items                       ERROR (0.00s)
    ActionController::RoutingError:         ActionController::RoutingError: uninitialized constant ItemsController
            test/features/to_do_list_test.rb:5:in `block (2 levels) in '
            test/features/to_do_list_test.rb:5:in `block (2 levels) in '
    
    Finished in 0.00736s
    1 tests, 0 assertions, 0 failures, 1 errors, 0 skips
    

    The route we just defined expects an ItemsController with an index action. It would be easy enough to just create or generate this, but our workflow dictates that first we need a failing test describing what that controller action should look like. At the moment, we’re interested in the shortest path to display a view with an empty list of items. That produces a controller test like this:

    require "test_helper"
    
    describe "ItemsController" do
      describe "GET :index" do
        before do
          get :index
        end
    
        it "renders items/index" do
          must_render_template "items/index"
        end
    
        it "responds with success" do
          must_respond_with :success
        end
      end
    end
    

    Most of the heavy lifting for the simple checks you see here is provided by Rails’ ActionController::TestCase with the spec-style expectations provided as syntactic sugar by the minitest-rails gem. Minitest’s spec syntax resembles that of earlier versions of RSpec before recent changes to the way specs are defined and the introduction of the expect syntax. The resulting code is terse but expressive and reads well with no unnecessary noise.

    Now when we run tests, we have errors in the feature and the two new controller tests.

    To Do List Feature Test
      test_0001_displays a list of to-do items                       ERROR (0.00s)
    ActionController::RoutingError:         ActionController::RoutingError: uninitialized constant ItemsController
            test/features/to_do_list_test.rb:5:in `block (2 levels) in '
            test/features/to_do_list_test.rb:5:in `block (2 levels) in '
    
    ItemsController::GET :index
      test_0001_renders items/index                                  ERROR (0.00s)
    NameError:         NameError: Unable to resolve controller for ItemsController::GET :index
    
      test_0002_responds with success                                ERROR (0.00s)
    NameError:         NameError: Unable to resolve controller for ItemsController::GET :index
    
    Finished in 0.01033s
    3 tests, 0 assertions, 0 failures, 3 errors, 0 skips
    

    Awesome, it’s practically raining errors! Since all of these are caused by the lack of an ItemsController, we can fix them by creating one. We’ll use the Rails generator – making sure not to overwrite the controller test we’ve already started working when prompted. Once that’s done, re-running the test yields the following output.

    To Do List Feature Test
      test_0001_displays a list of to-do items                       FAIL (0.02s)
    Minitest::Assertion:         expected to find #items.
            test/features/to_do_list_test.rb:6:in `block (2 levels) in '
    
    ItemsController::GET :index
      test_0001_renders items/index                                  PASS (0.00s)
      test_0002_responds with success                                PASS (0.00s)
    
    Finished in 0.03471s
    3 tests, 3 assertions, 1 failures, 0 errors, 0 skips
    

    The generated controller clears up both controller test errors for the time being, and the fact that there’s now a controller action and basic view has fixed the previous error and turned it into a failure. All that needs to happen now is to add an empty list of items to the view code by replacing the boilerplate view with:

    erb
    <%%= content_tag :div, class: 'items' do %>
    <%% end -%>
    

    And when we run the tests again:

    To Do List Feature Test
      test_0001_displays a list of to-do items                       PASS (0.02s)
    
    ItemsController::GET :index
      test_0001_renders items/index                                  PASS (0.00s)
      test_0002_responds with success                                PASS (0.00s)
    
    Finished in 0.03379s
    3 tests, 3 assertions, 0 failures, 0 errors, 0 skips
    

    All green! In only a few minutes, we’ve created a new application and driven our way to the first feature from start to finish using tests as a guide.

    This gives you a taste of how to get started building a new Rails application using BDD and Minitest. In part two of the series, we’ll dig in deeper and show how to add more realistic functionality to the application while driving the whole process through our tests.

    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

    This tutorial continues in “BDD on Rails with Minitest, Part 2: Implementing a Feature“.

    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.