Beginning Outside-In Rails Development with Cucumber and RSpec Posted on 14th February 2012 by Jared Carroll in Development, Process

The RSpec Book defines outside-in Rails development as starting with views and working your way in toward the models. By developing from the outside in, you are always taking a client perspective at each layer of the application. The end result is an absolute minimum implementation, consisting of simple, expressive interfaces.

Outside-in development doesn’t require any specific tool or language. This article will demonstrate it in Rails, using two popular testing tools: Cucumber and RSpec.

Start with a High-level Specification

Starting with a high-level specification requires you to have a clear understanding about what you want to achieve. If it’s still unclear, now is the time to have a conversation with the client. After establishing a clear goal, we can use Cucumber to turn a plaintext story into executable code.

Our sample story will be from a news site. The feature is a JSON API endpoint for news articles.

In order to reference published articles in other applications As an API client I want to be able to request articles via a JSON API

This story can be copied directly into a Cucumber feature.

features/api/v1/articles.feature

Feature: Articles API In order to reference published articles in other applications As an API client I want to be able to request articles via a JSON API Scenario: Get articles Given some published articles And some unpublished articles When I ask for articles from the API Then I should only receive published articles as JSON

Let’s run this feature to figure out what to do next.

$ cucumber features/api/v1/articles.feature Using the default profile... UUUU 1 scenario (1 undefined) 4 steps (4 undefined) 0m0.002s You can implement step definitions for undefined steps with these snippets: Given /^some published articles$/ do pending # express the regexp above with the code you wish you had end Given /^some unpublished articles$/ do pending # express the regexp above with the code you wish you had end When /^I ask for articles from the API$/ do pending # express the regexp above with the code you wish you had end Then /^I should only receive published articles as JSON$/ do pending # express the regexp above with the code you wish you had end

Fantasy Coding

Cucumber successfully parsed our feature but it needs definitions for all of our steps. Let’s implement these steps writing code that we wish already existed.

features/step_definitions/api/v1/articles_steps.rb

Given /^some published articles$/ do FactoryGirl.create_list :published_article, 3 end Given /^some unpublished articles$/ do FactoryGirl.create_list :unpublished_article, 3 end When /^I ask for articles from the API$/ do header 'Accept', 'application/json' get '/api/v1/articles' end Then /^I should only receive published articles as JSON$/ do articles_json = JSON last_response.body articles_json.should have(3).published_articles published_articles = Article.all.select {|article| article.published?} published_articles.should_not be_empty published_articles.each do |published_article| article_json = articles_json.detect do |article_json| article_json['title'] == published_article.title end article_json.should be end end

In our two Given steps, we establish a context consisting of published and unpublished articles. These two factories don’t exist yet; we just wrote the code we wish we had. This is a major benefit of developing from the outside in. By writing code that doesn’t even exist, you’ll end up creating ideal objects and interfaces.

In our When step, we exercise our application by first setting a proper HTTP header and then making an HTTP GET request to a non-existent URL. Again, this URL doesn’t exist, it’s just where we would expect the articles to be.

Finally in our Then step, we verify the response. Here we begin to discover our domain model; imagining an Article class with a #published? instance method.

Let Cucumber Guide the Way

With our steps defined, a rerun of Cucumber fails in our Given step because our factories don’t exist yet.

$ cucumber features/api/v1/articles.feature Using the default profile... F--- (::) failed steps (::) Factory not registered: published_article (ArgumentError) ./features/step_definitions/api/v1/articles_steps.rb:2:in `/^some published articles$/' features/api/v1/articles.feature:7:in `Given some published articles' Failing Scenarios: cucumber features/api/v1/articles.feature:6 # Scenario: Get articles

Let’s define these two factories.

spec/factories/articles.rb

FactoryGirl.define do factory :article do sequence(:title) {|n| "title-#{n}"} factory :published_article do published true end factory :unpublished_article do published false end end end

Cucumber guides us toward our next step.

$ cucumber features/api/v1/articles.feature Using the default profile... F--- (::) failed steps (::) uninitialized constant Article (NameError) ./features/step_definitions/api/v1/articles_steps.rb:2:in `/^some published articles$/' features/api/v1/articles.feature:7:in `Given some published articles' Failing Scenarios: cucumber features/api/v1/articles.feature:6 # Scenario: Get articles

Our factories are being automatically mapped to a non-existent Article class. We can use the Rails model generator to create this class. We’ll also specify the title and published attributes we referenced in our Then step.

$ rails g model article title published:boolean invoke active_record create db/migrate/20120213050147_create_articles.rb create app/models/article.rb invoke rspec create spec/models/article_spec.rb $ rake db:migrate db:test:prepare == CreateArticles: migrating ================================================= -- create_table(:articles) -> 0.0497s == CreateArticles: migrated (0.0498s) ========================================

With our setup passing, Cucumber now fails at our API request.

$ cucumber features/api/v1/articles.feature Using the default profile... ..F- (::) failed steps (::) No route matches [GET] "/api/v1/articles" (ActionController::RoutingError) ./features/step_definitions/api/v1/articles_steps.rb:11:in `/^I ask for articles from the API$/' features/api/v1/articles.feature:9:in `When I ask for articles from the API' Failing Scenarios: cucumber features/api/v1/articles.feature:6 # Scenario: Get articles

Drill Down to the Controller

We need to add a route for our API endpoint.

config/routes.rb

Sample::Application.routes.draw do namespace :api do namespace :v1 do resources :articles end end end

$ cucumber features/api/v1/articles.feature Using the default profile... ..F- (::) failed steps (::) uninitialized constant Api (ActionController::RoutingError) ./features/step_definitions/api/v1/articles_steps.rb:11:in `/^I ask for articles from the API$/' features/api/v1/articles.feature:9:in `When I ask for articles from the API' Failing Scenarios: cucumber features/api/v1/articles.feature:6 # Scenario: Get articles

With the routes in place, Cucumber tells us we’re missing a constant. This particular error message is from the Rails #namespace method we used in our routes file. Namespacing our controller will fix this issue. We can use the Rails controller generator to create a namespaced controller class.

$ rails g controller api::v1::articles create app/controllers/api/v1/articles_controller.rb invoke erb create app/views/api/v1/articles invoke rspec create spec/controllers/api/v1/articles_controller_spec.rb invoke helper create app/helpers/api/v1/articles_helper.rb invoke rspec create spec/helpers/api/v1/articles_helper_spec.rb invoke assets invoke coffee create app/assets/javascripts/api/v1/articles.js.coffee invoke scss create app/assets/stylesheets/api/v1/articles.css.scss

Cucumber fails again, this time looking for an #index action in our controller.

$ cucumber features/api/v1/articles.feature Using the default profile... ..F- (::) failed steps (::) The action 'index' could not be found for Api::V1::ArticlesController (AbstractController::ActionNotFound) ./features/step_definitions/api/v1/articles_steps.rb:11:in `/^I ask for articles from the API$/' features/api/v1/articles.feature:9:in `When I ask for articles from the API' Failing Scenarios: cucumber features/api/v1/articles.feature:6 # Scenario: Get articles

At this point, some outside-in practitioners will also drop down a level with respect to testing and use RSpec to spec out the controller. Since we’re new to outside-in development, I’m going to skip this step and go straight to the implementation. We’ll look at the value of controller specs and when it makes sense to write them later on.

app/controllers/api/v1/articles_controller.rb

class Api::V1::ArticlesController < ApplicationController def index end end

$ cucumber features/api/v1/articles.feature Using the default profile... ..F- (::) failed steps (::) Missing template api/v1/articles/index, application/index with {:locale=>[:en], :formats=>[:json], :handlers=>[:erb, :builder, :jbuilder, :coffee]}. Searched in: * "/Users/jared/Projects/sample/app/views" (ActionView::MissingTemplate) /Users/jared/.rbenv/versions/1.9.2-p290/lib/ruby/1.9.1/benchmark.rb:310:in `realtime' ./features/step_definitions/api/v1/articles_steps.rb:11:in `/^I ask for articles from the API$/' features/api/v1/articles.feature:9:in `When I ask for articles from the API' Failing Scenarios: cucumber features/api/v1/articles.feature:6 # Scenario: Get articles

Now we fail because our #index action needs a template. Let’s create an empty template just so we can finally get a non-infrastructure related failure, i.e., a logic error, from Cucumber.

$ touch app/views/api/v1/articles/index.json.jbuilder $ cucumber features/api/v1/articles.feature Using the default profile... ...F (::) failed steps (::) expected 3 published_articles, got 0 (RSpec::Expectations::ExpectationNotMetError) ./features/step_definitions/api/v1/articles_steps.rb:16:in `/^I should only receive published articles as JSON$/' features/api/v1/articles.feature:10:in `Then I should only receive published articles as JSON' Failing Scenarios: cucumber features/api/v1/articles.feature:6 # Scenario: Get articles

Drill Down to the View

With the routing and request handling boilerplate out of the way, we finally get a “legitimate” failure from Cucumber. At this point, some outside-in practitioners will drop down a level with respect to testing and use RSpec to spec out the view. Like controller specs, I’m going to skip this step for simplicity. We’ll discuss when a view spec makes sense later on. For now, let’s update our blank view to actually render some JSON (we’ll use the jbuilder Gem to construct the JSON).

app/views/api/v1/articles/index.json.jbuilder

json.array! @articles do |json, article| json.title article.title end

Again, we write this view imagining an “articles” instance variable; ideally, a collection containing our articles. This helps us avoid setting up unnecessary state in our action.

Cucumber fails again, this time with a long stacktrace (abbreviated below) originating in jbuilder .

$ cucumber features/api/v1/articles.feature Using the default profile... ..F- (::) failed steps (::) undefined method `empty?' for nil:NilClass (ActionView::Template::Error) Failing Scenarios: cucumber features/api/v1/articles.feature:6 # Scenario: Get articles

This failure is because the instance variable doesn’t exist yet. Let’s update our Api::V1::ArticlesController#index action to find all published articles.

app/controllers/api/v1/articles_controller.rb

class Api::V1::ArticlesController < ApplicationController def index @articles = Article.published end end

We’ve kept our controller thin and decided to not directly test it. By keeping controller logic to a minimum, skipping controller tests isn’t a significant risk.

Cucumber now guides us to our domain model.

$ cucumber features/api/v1/articles.feature Using the default profile... ..F- (::) failed steps (::) undefined method `published' for #<Class:0x007ff13c529498> (NoMethodError) ./app/controllers/api/v1/articles_controller.rb:3:in `index' ./features/step_definitions/api/v1/articles_steps.rb:11:in `/^I ask for articles from the API$/' features/api/v1/articles.feature:9:in `When I ask for articles from the API' Failing Scenarios: cucumber features/api/v1/articles.feature:6 # Scenario: Get articles

Drill Down to the Model

Despite skipping controller and view specs, I do feel it’s beneficial to drill down a layer in our tests and directly test the model. Model tests will shorten our testing feedback loop and allow us to specify at a level closer to the code. Skipping model tests and relying on Cucumber, keeps the feedback loop too large, slowing you down.

spec/models/article_spec.rb

require 'spec_helper' describe Article do describe '.published' do before do FactoryGirl.create_list(:published_article, 2) FactoryGirl.create_list(:unpublished_article, 1) @published = Article.published end it 'only returns published articles' do @published.should_not be_empty @published.each do |article| article.should be_published end end end end

RSpec will now be our guide.

$ rspec spec/models/article_spec.rb F Failures: 1) Article.published only returns published articles Failure/Error: @published = Article.published NoMethodError: undefined method `published' for #<Class:0x007fee85f72440> # ./spec/models/article_spec.rb:9:in `block (3 levels) in <top # (required)>' Finished in 0.03521 seconds 1 example, 1 failure

app/models/article.rb

class Article < ActiveRecord::Base def self.published end end

$ rspec spec/models/article_spec.rb F Failures: 1) Article.published only returns published articles Failure/Error: @published.should_not be_empty NoMethodError: undefined method `empty?' for nil:NilClass # ./spec/models/article_spec.rb:13:in `block (3 levels) in <top # (required)>' Finished in 0.03338 seconds 1 example, 1 failure

app/models/article.rb

class Article < ActiveRecord::Base def self.published where :published => true end end

$ rspec spec/models/article_spec.rb . Finished in 0.10455 seconds 1 example, 0 failures

With Article.published specified, we can return to Cucumber.

Jump Back Up to Cucumber

Cucumber is now passing and our story is complete.

$ cucumber features/api/v1/articles.feature Using the default profile... .... 1 scenario (1 passed) 4 steps (4 passed)

The complete code for this example can be found on github.

Change Your Perspective

I use the above approach on every feature I write. At each layer, I find myself getting lazier and lazier, delaying the hard work until the very end. At that point, I’m in the domain model, the heart of the application, and where the majority of logic should be. By taking a client perspective at each layer, the resulting objects remain simple, have expressive interfaces, and logic naturally finds its home.

You can begin developing from the outside in at every one of your application’s interfaces. The example above demonstrated a JSON API. Traditional HTML interfaces can easily be tested using capybara. And if you’re developing a command line interface, perhaps for a Ruby gem, take a look at aruba.

Do I Need to Test at Every Layer?

The RSpec book suggests writing tests at each layer, i.e., view specs, controller specs, helper specs, and model specs. I’ve tried this approach several times but I usually feel all the lower level specs, except model specs, aren’t worth it. They do shorten the testing feedback loop, but their reliance on stubbing and mocking to achieve true isolation makes refactoring and maintenance difficult. I also keep the logic to such a minimum in these objects, e.g., controllers, that the additional fine-grained unit tests don’t provide that much benefit.

There is nothing wrong with not unit testing each part of your application. Don’t dogmatically insist that everything be unit tested. Oftentimes a higher-level integration test will sufficiently exercise (albeit indirectly) a particular piece of code. The tradeoff here is that your testing feedback loop will be large. You’ll need to execute a full-stack Cucumber test just to see if a change you made, perhaps in a controller or a view, passes your failing test. Occasionally, I’ll use a lower level view or controller test to handle an edge case but this is a pretty rare occurrence. This is just my personal style. I would recommend trying out testing at every level, especially view and controller tests, to see how it feels and if it’s beneficial for you and your team.

Give It A Try

Outside-in development often feels strange to newcomers. Most developers prefer to start with the “important” part of an application, i.e., the domain model, and work their way outwards. Thinking like a server and not a client can lead to overengineering by implementing more than you need. Your resulting objects and their interfaces will also be less than optimal, or at least take longer to get quite right.

Like most things in Rails, the tools for developing outside-in are easy to setup and configure. If Cucumber or RSpec aren’t your thing, adapt your favorite tool and give outside-in development a try.