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

· 27 Oct 2014 · Semaphore Engineering Blog

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:

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 #<#<Class:0x007f83c41efee8>:0x007f83c4153930>
        test/features/to_do_list_test.rb:5:in `block (2 levels) in <top (required)>'
        test/features/to_do_list_test.rb:5:in `block (2 levels) in <top (required)>'

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.

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

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

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 <top (required)>'

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:

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

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

comments powered by Disqus

Semaphore Blog

Fresh updates from your friendly continuous integration and deployment service. Product news, engineering insights and more.

NEW

We've launched Boosters, free one-click solution for CI parallelization. Read more…

Newsletter

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

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