giant robots smashing into other giant robots

We are thoughtbot. We make web & mobile apps.

Comments (View)

history: ruby integration testing

Productivity and happiness. That’s why Ruby and Rails were written, why developers were drawn to them, and why we keeping using them. They continue to be the stated goals on ruby-lang.org and rubyonrails.org.

Over time, both cultures have developed a very strong testing culture, and more recently a strong integration testing culture, but the search is still on for more productive, happier approaches.

ruby integration testing history

This is by no means comprehensive, and is completely skewed by my personal opinions and biases, but here’s a brief overview of Ruby integration test history:

2006

Rails 1.1 introduces baked-in integration tests. Many developers are still getting used to testing, however, and the announcement post gets less than 10 comments.

It exercises the full Rails stack with methods like get, post, follow_redirect!, xml_http_request, status, path, and open_session.

The intention is that developers use normal Test::Unit methods such as assert_equal to verify expectations on the resulting HTTP response headers and body.

Rails developers can now write:

class ExampleTest < ActionController::IntegrationTest
  def test_login
    get "/login"
    assert_equal 200, status

    post "/login",
      :username => people(:jamis).username,
      :password => people(:jamis).password
    follow_redirect!

    assert_equal 200, status
    assert_equal "/home", path
  end
end

2007

RSpec introduces the Story Runner. The idea of customers writing and/or reading story input and/or output can now be tested in practice instead of talked about theoretically.

Rails developers can now write:

Scenario "Root user" do
  Given "a user named", "admin"
  And "a company named", "Company1"
  And "a company named", "Company2"

  And "the user has the role", "root" do |role|
    @user.update_attribute :role, role
  end
  And "logged in as", "admin"

  When "visiting", "/"

  Then "viewer should see", "main/root_home"
  And  "page should show company named", "Company1" do |name|
    response.should have_text(/#{name}/)
  end
  And  "page should show company named", "Company2"
end

Bryan Helmkamp introduces Webrat. It simulates the browser similarly to Rails integration tests but has a better-looking DSL and adds new, fine-grained interaction such as click_link, fill_in, and click_button.

Over time, Webrat will add very strong leverage of Nokogiri to verify HTML responses using XPath and CSS selectors. Developers rejoice.

Rails developers can now write:

def test_sign_up
  visits "/"
  clicks_link "Sign up"
  fills_in "Email", :with => "good@example.com"
  select "Free account"
  clicks_button "Register"
  ...
end

2008

Merb recommends using the request method for integration tests. This preserves state across requests, is easier than the older dispatch_to, and comes with some nice RSpec matchers such as be_successful and redirect_to.

Cucumber replaces Story Runner. Every Ruby conference hereafter includes a talk on Cucumber and it maintains a top spot on Github.

Rubyists can now write:

Feature: Addition
  In order to avoid silly mistakes
  As a math idiot 
  I want to be told the sum of two numbers

  Scenario: Add two numbers
    Given I have entered 2 into the calculator
    And I have entered 2 into the calculator
    When I press add
    Then the result should be 4 on the screen

When used in combination with a Rails app, this style heavily uses pattern-matching to map executable English (or one of many supported languages) to code.

The English:

Given I signed up with "email@person.com/password"

The pattern match:

Given /^I signed up with "(.*)\/(.*)"$/ do |email, password|
  user = Factory :user,
    :email                 => email,
    :password              => password,
    :password_confirmation => password
end 

2009

Integration testing is firmly mainstream.

Bryan Helmkamp releases Rack::Test. Like Rack itself, it aims to provide a common interface for test frameworks to interact with Rack apps.

Rack::Test pays homage to the frameworks before it, using methods that match the HTTP verbs: get, post, put, and delete, head) and adding request (Merb) and follow_redirect! (Rails). It also handles HTTP authentication and stores requests and responses in last_request and last_response.

Rack developers can now write:

class HelloWorldTest < Test::Unit::TestCase
  include Rack::Test::Methods

  def app
    Sinatra::Application
  end

  def test_it_says_hello_world
    get '/'
    assert last_response.ok?
    assert_equal 'Hello World', last_response.body
  end

  def test_it_says_hello_to_a_person
    get '/', :name => 'Simon'
    assert last_response.body.include?('Simon')
  end
end

Cucumber and Webrat are increasingly used widely by Rails developers for integration tests. Its costs and benefits are debated based on actual experience with it, and alternatives begin to emerge for common reasons.

Gibbon is created. It brings Cucumber’s widely appreciated Given/When/Then DSL to regular RSpec. It is somewhat of a throwback to the Story Runner. It depends on Webrat to simulate the browser. It looks like this:

Feature "A Tomatoist does a pomodoro" do
  Story <<-eos
  In order to perform a focused unit of work
  As a Tomatoist
  I want to start a pomodoro
  eos

  Scenario "Starting a pomodoro" do
    When "I go to the home page" do
      executes { visit '/' }

      Then "I should be sent to a new session" do
        current_url.should =~ /\/\w{3,}/
      end

      And "I should see an unstarted timer" do
        response.should have_tag('#timer .countdown_row','00:00')
      end

      When "I click the pomodoro button" do
        executes do
          @session_url = current_url
          click_button 'Pomodoro'
        end

        Then "I should be on my session's page" do
          current_url.should == @session_url
        end

        And "my timer history should show the current pomdoro" do
          response.should have_tag('#history ul li', /Pomodoro/, 1)
        end
      end
    end
  end
end

Hashrocket converts the codenamed Gibbon into a library called Unencumbered.

Evan Light releases Coulda. It brings the Given/When/Then DSL to Test::Unit. It depends on Webrat or another library to simulate the browser. It looks like this:

Feature "Painfully obvious" do
  in_order_to "demonstrate a simple test"
  as_a "coulda developer"
  i_want_to "provide a straight-forward scenario"

  Scenario "Describing something obvious" do
    Given "something without a value" do
      @no_value = nil
    end

    When "I give it a value" do
      @no_value = true
    end

    Then "it should have a value" do
      assert(@no_value)
    end
  end
end

Jim Weireich experiments with Given. It brings the Given/When/Then DSL to Test::Unit. It depends on Webrat or another library to simulate the browser. It introduces the concept of an Invariant. It looks like this:

class StackBehavior < Given::TestCase
  Invariant { expect(@stack.depth) >= 0 }
  Invariant { expect(@stack.empty?) == (@stack.depth == 0) }

  def empty_stack
    @stack = Stack.new
  end

  Given(:empty_stack) do
    Then { expect(@stack.depth) == 0 }

    When { @stack.push(:an_item) }
    Then { expect(@stack.depth) == 1 }
    Then { expect(@stack.top) == :an_item }

    When { @stack.pop }
    FailsWith(Stack::UsageError)
    Then { expect(exception.message) =~ /empty/ }
  end
end

I write about 50 lines of horrible hacks. It brings the Given/When/Then DSL to Test::Unit. It relies on Webrat and Rack::Test to simulate the browser and theoretically work for all Rack apps (only tested on Sinatra). I keep the nesting totally flat, as is my style. It looks like this:

Feature 'Shorten URL' do
  Given 'I am on the homepage' do
    visit '/'
  end

  When 'I submit http://example.com' do
    fill_in      'url', :with => 'http://example.com'
    click_button 'shorten'
  end

  Then 'I should see a short link' do
    assert_have_selector 'a#short'
  end

  When 'I follow the short link' do
    click_link 'short'
  end

  Then 'I should be on http://example.com' do
    assert_equal 'http://example.com', current_url
  end
end

so what?

That’s quite a history. Looking back, we understand what makes programmers happy and unhappy about integration testing.

what’s making programmers unhappy?

Dependencies. Cucumber 0.4.4 has runtime dependencies of term-ansicolor, treetop, polyglot, builder, and diff-lcs. There’s inevitably small headaches and incompatibilities as dependencies increase.

Pattern-matching. Cucumber’s goal of “executable documentation” (good goal, no criticism there) lead it to an advanced pattern-matching style of programming. This is the number one complaint I’ve heard about it, leading to related complaints of:

  • difficult to navigate the plethora of feature files and step definition files
  • indirection makes debugging hard
  • pattern matching “feels wrong”
  • decreased productivity

This last point is most damning but also most anecdotal and personal.

Integration testing in general has two flaws that are inherent in integration testing itself and unrelated to any tool:

  • they are slower to run, as they execute more of the stack
  • they are slower to write, as you have to write more to get a single feature passing

That’s just part of the deal, though.

what’s making programmers happy?

Normal “method programming”. Many programmers are placing lower value on “executable documentation” compared to using familiar tools, seeing method names directly adjacent to the implementation, and relying on familiar programming techniques such as intention revealing names and normal method extraction.

Small DSLs that pay homage to Given/When/Then. Everyone is looking to re-use this in other environments, usually layering a syntax of some kind on top of their test framework of choice. Variations include the super-flat (what I did), the super-RSpec’y (Gibbon), and “Cucumber for Test::Unit” (Coulda, the one actual library of the group).

Re-phrased: syntax matters to programmers.

Favorite old tools. We may be “agile”, but if we like something, we’re loathe to change.

CSS selectors. Parsing the response body with CSS selectors in Rails’ assert_select and Webrat’s assert_have_selector breeds familiarity with old skills. I’ve seen this pattern crop up again in Effigy.

Single responsibility. I have to hand it to Bryan Helmkamp. Looking back over the history of these integration testing tools, his name really sticks out as the one leading innovation in the subset concerns of:

  • simulated browser
  • interface between testing tools and Rack apps

He also clearly took into account the history of integration testing in various Ruby web frameworks.

what’s next?

What about Ruby integration testing is currently making you happy and productive?