BDD on Rails with Minitest, Part 1: Up and Running
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.
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 #<#<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:
- 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".