Pivotal Labs

Main menu

Skip to primary content
Skip to secondary content
  • About
  • Case Studies
  • Team
    • Executives
    • Locations
      • San Francisco (HQ)
      • Boston
      • Boulder
      • Denver
      • London
      • Los Angeles
      • New York
  • Community
    • Blogs
    • Tech Talks
    • Events
  • Careers
    • Lifestyle
    • Principles & Practices
    • Benefits
    • FAQ
    • Apply
  • Contact
    • Press Room
    • Press Releases
    • In The News
    • Press Kit
  • All
  • Labs
  • Standup
  • Tracker
Pivotal Labs

Standup 3/26/2009: Testing Request Headers When Request Object Is Frozen

Pivotal Labs
Thursday, March 26, 2009

Ask for Help

“How do you test request headers? The request object is frozen…”

The team is using rspec to test an OAuth implementation and needs better access to the request object.

  • Possibly modify the request environment prior to running the test -or-
  • Instantiate a new, non-frozen request object.
  • 0 Shares
  • Share on Facebook
  • Share on Twitter
Pivotal Labs

Standup 3/25/2009: Branches + JSUnit + CI + IE = :-(

Pivotal Labs
Wednesday, March 25, 2009

Interesting Things

  • Branches + JsUnit + CI + IE = :-( : Apparently it is difficult to manage IE’s cache in CI. One project apparently has a bat file on CI that clears the cache every 30 minutes. Another team solved this by making the cache directory read only. Often browser/OS combinations have some technique for disabling caching.
  • Test Swarm Alpha: this is a crowd sourced javascript testing solution (think seti@home for javascript testing) being developed by John Resig.

Ask for Help

“AR attribute appears to be skipped by text field helper?!” Apparently the model method is bypassed by the text field helper if a column of the same name is present in the underlying table. This was experienced in Rails 2.2.

Others have apparently experienced this in the past but a clear answer did not surface.

  • 0 Shares
  • Share on Facebook
  • Share on Twitter
Joe Moore

DRY, Targeted, and Reusable Testing of ActiveRecord Extensions

Joe Moore
Wednesday, March 18, 2009

At Pivotal, we are passionate about test driven development, keeping things DRY, and writing readable and understandable code. Satisfying all of these desires can be challenging, especially when writing test code. In particular, ActiveRecord extensions present several challenges: which models using an extension should we test? How do we both test our extension in isolation while also testing all model’s usage of that extension? Is it even worth it?

The answer is yes, it is worth it, and it’s also fairly easy, readable, understandable, and DRY. I will present both a common problem and a solution, using a cumulation of technologies and techniques from multiple Pivotal projects, in particular using acts_as_fu to create laser-targeted, isolated, and disposable ActiveRecord models for testing extensions and RSpec shared behaviors to minimize the amount of duplicated test code.

Often we find common patterns in ActiveRecord models and we wish to share that functionality by mixing in a module of shared code, or even mixing that module in to ActiveRecord::Base itself. How should we go about testing these ActiveRecord extensions? Not only do we want to test the extension, but also test that models using that extension are doing so properly. We’ve blogged about dynamically creating ActiveRecords to test extensions in the past, but Pivot Pat Nakajima’s acts_as_fu plugin is a far better tool for this.

The Setup

Let’s say we have three models: Pivotal, Lab, and Pivot. Both a Pivot and a Lab belongs_to Pivotal, have a name, nickname, some common validation, etc:

# app/models/pivotal.rb
class Pivotal < ActiveRecord::Base
end

# app/models/lab.rb
class Lab < ActiveRecord::Base
  RANDOM_NICKNAMES = ["New Hotness", "LOL-Cat Factory", "Tweet Machine"]
  belongs_to :pivotal
  validates_presence_of :name

  def nickname
    "#{self.name}, 'The #{RANDOM_NICKNAMES[rand(RANDOM_NICKNAMES.length)]}'"
  end
end

# app/models/pivot.rb
class Pivot < ActiveRecord::Base
  RANDOM_NICKNAMES = ["New Hotness", "LOL-Cat Factory", "Tweet Machine"]
  belongs_to :pivotal
  validates_presence_of :name

  def nickname
    "#{self.name}, 'The #{RANDOM_NICKNAMES[rand(RANDOM_NICKNAMES.length)]}'"
  end
end

The specs for Lab and Pivot might look like the following:

# spec/models/lab_spec.rb
describe Lab do
  before(:each) do
    @lab = Lab.new
  end

  it "should require name" do
    @lab.should have(1).errors_on(:name)
    @lab.name = 'Stealth Startups'
    @lab.should have(0).errors_on(:name)
  end

  it "should generate a random nickname" do
    @lab.name = 'Stealth Starup'
    @lab.nickname.should_not be_blank
    @lab.nickname.should include(@lab.name + ", 'The ")
  end

  it "should belong to Pivotal" do
    @lab.should respond_to(:pivotal)
    @lab.should respond_to(:pivotal=)
    @lab.should respond_to(:pivotal_id)
    @lab.should respond_to(:pivotal_id=)
  end
end

# spec/models/pivot_spec.rb
describe Pivot do
  before do
    @pivot = Pivot.new
  end

  it "should require name" do
    @pivot.should have(1).errors_on(:name)
    @pivot.name = 'Joe Moore'
    @pivot.should have(0).errors_on(:name)
  end

  it "should generate a random nickname" do
    @pivot.name = 'Joe Moore'
    @pivot.nickname.should_not be_blank
    @pivot.nickname.should include(@pivot.name + ", 'The ")
  end

  it "should belong to Pivotal" do
    @pivot.should respond_to(:pivotal)
    @pivot.should respond_to(:pivotal=)
    @pivot.should respond_to(:pivotal_id)
    @lab.should respond_to(:pivotal_id)
  end
end

DRYing Up the Models

Yuck, look at all that duplication! Let’s start eliminating it by pulling the common model code into an ActiveRecord extension named belongs_to_pivotal:

# lib/belongs_to_pivotal.rb
module BelongsToPivotal
  RANDOM_NICKNAMES = ["New Hotness", "LOL-Cat Factory", "Tweet Machine"]
  module ClassMethods
    def belongs_to_pivotal
      belongs_to :pivotal
      validates_presence_of :name

      instance_eval do
        include BelongsToPivotalInstanceMethods
      end
    end
  end

  module BelongsToPivotalInstanceMethods
    def nickname
      "#{self.name}, 'The #{BelongsToPivotal::RANDOM_NICKNAMES[rand(BelongsToPivotal::RANDOM_NICKNAMES.length)]}'"
    end
  end

  def self.included(base)
    base.extend(ClassMethods)
  end
end

Now our Models look like this:

# app/models/lab.rb
class Lab < ActiveRecord::Base
  belongs_to_pivotal
end

# app/models/pivot.rb
class Pivot < ActiveRecord::Base
  belongs_to_pivotal
end

You’ll need to add “ActiveRecord::Base.send :include, BelongsToPivotal” to config/initializers/new_rails_defaults.rb or some other initializer.

Testing the Extension with acts_as_fu

The models are looking better, but what about the specs? In the “old days” I would create a spec named belongs_to_pivotal_spec.rb and use one of the two Models in that spec. But, when you do that, you get all the the “baggage” from that Model, such as any other methods, associations, inherited methods and properties, etc. Let’s use acts_as_fu to write a spec that tests BelongsToPivotal in isolation.

# spec/lib/belongs_to_pivotal_spec.rb
describe BelongsToPivotal do
  before(:all) do
    # Using acts_as_fu to create a model specifically for our extension
    build_model :belongs_to_pivotal_models do
      # we will need these columns in the database
      string :name
      integer :pivotal_id

      # Call our extension here
      belongs_to_pivotal
    end
  end

  before(:each) do
    @pivotal_model = BelongsToPivotalModel.new
  end

  # Look, it's all of the model specs!
  it "should require name" do
    @pivotal_model.should have(1).errors_on(:name)
    @pivotal_model.name = 'Pivotal Model'
    @pivotal_model.should have(0).errors_on(:name)
  end

  it "should generate a random nickname" do
    @pivotal_model.name = 'Pivotal Model'
    @pivotal_model.nickname.should_not be_blank
    @pivotal_model.nickname.should include(@pivotal_model.name + ", 'The ")
  end

  it "should belong to Pivotal" do
    @pivotal_model.should respond_to(:pivotal)
    @pivotal_model.should respond_to(:pivotal=)
    @pivotal_model.should respond_to(:pivotal_id)
    @pivotal_model.should respond_to(:pivotal_id=)
  end
end

Now that our ActiveRecord extension is well tested, how do we make sure that our two models are actually using it? One technique is to check that each model responds to the specific methods added by our extension:

#spec/models/lab_spec.rb
describe Lab do
  ...
  it "should belong_to_pivotal" do
    @lab.should respond_to(:pivotal)
    @lab.should respond_to(:pivotal=)
    @lab.should respond_to(:pivotal_id)
    @lab.should respond_to(:pivotal_id=)
    @lab.should respond_to(:name)
    @lab.should respond_to(:name=)
    @lab.should respond_to(:nickname)
  end
  ...

This does not feel very satisfying. We are duplicating some of the tests from belongs_to_pivotal_spec.rb and not verifying that we are getting the validations. A crazy coincidence could result in these methods all being defined without actually using our extension.

Another technique, though some would call it a hack, is to provide a hook within the extension itself so we can check for it later:

# lib/belongs_to_pivotal.rb
module BelongsToPivotal
  ...
  module ClassMethods
    ...
    # We can check this to see if a model uses this extension
    def belongs_to_pivotal?
      self.included_modules.include?(BelongsToPivotalInstanceMethods)
    end
  end
  ...
end

Let’s update belongs_to_pivotal_spec.rb to test this method:

# spec/lib/belongs_to_pivotal_spec.rb
describe BelongsToPivotal do
  before(:all) do
    # Using acts_as_fu to create a model specifically for our extension
    build_model :belongs_to_pivotal_models do
      ...
    end

    # Create a model that does not use our extension
    build_model :never_belongs_to_pivotal_models do
      # do nothing
    end
  end
  ...
  it "should know if it belongs_to_pivotal" do
    BelongsToPivotalModel.belongs_to_pivotal?.should be_true
    NeverBelongsToPivotalModel.belongs_to_pivotal?.should be_false
  end
end

#spec/models/lab_spec.rb
describe Lab do
  it "should belong_to_pivotal" do
    Lab.belongs_to_pivotal?.should be_true
  end
end

#spec/models/pivot_spec.rb
describe Pivot do
  it "should belong to Pivotal" do
    Pivot.belongs_to_pivotal?.should be_true
  end
end

Using RSpec Shared Behaviors

How much further can we go? Notice that our two Model specs are 5 whole lines long! Unacceptable! All kidding aside, we can DRY this up just a bit more by using RSpec’s shared behaviors.

In spec/spec_helper.rb

# spec/spec_helper.rb
describe 'it belongs to pivotal', :shared => true do
  it "should belongs_to_pivotal" do
    described_class.belongs_to_pivotal?.should be_true
  end
end

Now we can use this shared behavior in our specs:

#spec/models/lab_spec.rb
describe Lab do
  it_should_behave_like "it belongs to pivotal"
end

#spec/models/pivot_spec.rb
describe Pivot do
  it_should_behave_like "it belongs to pivotal"
end

I hope that these techniques are helpful. Feel free to post your own!

  • 0 Shares
  • Share on Facebook
  • Share on Twitter
Pivotal Labs

Run JavaScript in Selenium tests. Easily.

Pivotal Labs
Thursday, March 5, 2009

Here’s the gist of this post: gist.github.com/58876

Ever since I’ve started using Webrat, a lot of the pain of Selenium has gone away
for me. There’s still a little bit of pain though. Part of it is caused by the fact
that it’s harder than it should be to just execute arbitrary bits of JavaScript in
in your current window under test. Well no more. Here’s a helper:

module SeleniumHelpers
  # Execute JavaScript in the context of your Selenium window
  def run_javascript(javascript)
    driver.get_eval <<-JS
      (function() {
        with(this) {
          #{javascript}
        }
      }).call(selenium.browserbot.getCurrentWindow());
    JS
  end

  private

  # If running in regular Selenium context, get_eval is defined on self.
  def driver
    respond_to?(:selenium) ? send(:selenium) : self
  end
end

To use it with Cucumber, do like so:

World do |world|
  world.extend(SeleniumHelpers)
  world
end

To use it with POS, do like so:

class JavaScriptHelperTest < SeleniumTestCase
  include SeleniumHelpers

  # your tests go here...
end

Now what?

Now to run JavaScript in your Selenium window, just call run_javascript. Note
that it’s always going to return a String, so you may have to massage the output
a tad:

checked_boxes_count = run_javascript <<-JS
  jQuery('input[type=checkbox]:checked').size();
JS

checked_boxes_count         # => "3"
checked_boxes_count.to_i   # => 3

Cooler stuff

While Webrat’s DSL for traversing web apps is awesome, I’ve always found the
alternatives (Polonium for example) to not jive well with how I think. They’re
way better than talking directly to Selenium, you’re still locked in to a certain
style. The run_javascript helper makes it easier to write your own helpers that
fit your own style.

module ElementHelpers
  class Element
    def initialize(context, selector)
      @context, @selector = context, selector
    end

    def hide!
      call(:hide)
    end

    def show!
      call(:show)
    end

    def visible?
      call(:is, ':visible') == 'true'
    end

    private

    def call(fn, *args)
      @context.run_javascript <<-JS
        return jQuery(#{@selector.inspect})[#{fn.to_s.inspect}](#{args.map(&:inspect).join(', ')});
      JS
    end
  end

  def locate(selector)
    Element.new(self, selector)
  end
end

Now you can write your tests like so:

class JavaScriptHelperTest < ActiveSupport::TestCase
  include SeleniumHelpers
  include ElementHelpers

  def setup
    @element = locate('#all')
  end

  def test_visible_by_default
    assert @element.visible?
  end

  def test_hide_element
    @element.hide!
    assert ! @element.visible?
  end

  def test_show_element
    @element.hide! # setup
    @element.show!
    assert @element.visible?
  end
end

Credit should go to Brian Takita, since he did most of the hard work and I just wrote a method. Let me
know if you have any issues or ideas with the helper, and may all your tests be green.

  • 0 Shares
  • Share on Facebook
  • Share on Twitter
Adam Milligan

Milligan's Law

Adam Milligan
Sunday, February 8, 2009

I’m not sure a person can name a law after himself, but if I had a law I would want it to be this:

Any non-additive change to non-test code that causes no test failures is a valid change and does not reduce the correctness of the code.

By extension, the first corollary would have to be this:

The full definition of correct behavior of code exists in the tests for that code.

Think about it.

  • 0 Shares
  • Share on Facebook
  • Share on Twitter
Adam Milligan

Silly gents, it is doing a good job (7)

Adam Milligan
Saturday, February 7, 2009

I recently published the article There is no Agile, in which I stated that the principles of ‘Agile’ are nothing more than “a collection of good ideas, based on years of collective experience, for improving how we do our jobs.” As an example, consider testing. Thorough testing is a ubiquitous principle of ‘Agile;’ thus, by logical extension, writing tests is a fundamentally important part of writing software well.

In short, if you write software, and you’re not writing tests, then you’re not doing your job.

I don’t mean not doing your job like a waiter who doesn’t take your order quickly enough; I mean not doing your job like a surgeon who operates without washing his or her hands. As in completely unacceptable performance.

As a corollary, if you’re not writing high quality tests, then you’re doing your job poorly. If you don’t achieve appropriate test coverage then you’re doing your job poorly. If you focus excessively on verification, and ignore the other benefits that tests can provide, then you’re doing your job poorly.

Several authors and accomplished software writers have cataloged the ways that thorough testing will lead to better design, the ability to refactor, improved resilience in the face of changing requirements, fewer defects, etc. In order to justify not writing tests the costs would have to overshadow these advantages.

I can’t think of a valid argument for not writing tests, although I’ve heard many attempts:

  • “It takes too long.” Consider that around 75% of the time spent on average software projects is bug-fixing and maintenance.
  • “This code won’t change.” It will. And it should.
  • “I know my code works.” Will it work with my code? With the code you write six months from now? When the functionality changes? (see above)
  • “It’s a prototype.” Fine. Throw it out when you’re done and use what you learned to write tested code.
  • “It’s not testable.” Unlikely. Far more likely you simply haven’t though of a way to test it.
  • “That’s what QA is for.” This statement shows so little respect for the people you presumably work with it warrants no response.
  • “I can’t be bothered.” McDonald’s is hiring.

In my experience, people who denounce the value of tests are nearly always people with little experience writing tested code. It’s time we stop allowing ourselves to determine how we do our job based on who has the biggest ego, who can shout the loudest, or who is the most resistant to change.

All this leads to the question that every single person reading this should be asking: writing high quality tests can be very difficult; how do we teach that?

  • 0 Shares
  • Share on Facebook
  • Share on Twitter
Abhijit Hiremagalur

Standup 01/12/2009: require & class reloading, acts_as_fu

Abhijit Hiremagalur
Monday, January 12, 2009

Interesting Things

  • Using ‘require’ explicitly interferes with class reloading in Rails

Frederick Cheung discusses this in more detail here. This might be related to the Selenium + class reloading issues some pivots have experienced in recent weeks. The alternative is to rely on Rails automagic loading or ‘require_dependency’.

  • acts_as_fu makes writing database independent tests for models is easier

Props to pivot Pat Nakajima for creating acts_as_fu.

  • 0 Shares
  • Share on Facebook
  • Share on Twitter
Chad Woolley

Automated End-to-end Integration Testing for ActiveResource APIs

Chad Woolley
Thursday, January 8, 2009

By popular demand, we’re working on making the Pivotal Tracker API ActiveResource-compliant.

However, there are some quirks that are required to make ActiveResource happy. For example, when you are doing a ‘create’ or ‘update’ request, ActiveResource wants the response location to point to the ‘show’ URL for the new or updated record. For example, here’s an ActiveResource ‘create’ call:

new_story = Story.create(
  :name => "New Story",
  :requested_by => "Dan",
  :description => "Make API ActiveResource compliant")

On the controller, you must add the :location option to the render – you can’t redirect:

render  :xml => xml,
        :location => service_project_story_url(service_id, project_id, @story),
        :status => status

…otherwise, you get this helpful error from ActiveResource:

/Library/Ruby/Gems/1.8/gems/activeresource-2.2.2/lib/active_resource/base.rb:1006
        :in `id_from_response': undefined method `[]' for nil:NilClass (NoMethodError)
        from /Library/Ruby/Gems/1.8/gems/activeresource-2.2.2/lib/active_resource/base.rb:993:in `create'

This is the type of error which you will only catch through end-to-end testing with a real ActiveResource client hitting the running app. When I did the initial spike to see what problems we would run into, I wrote a simple manual script to run against the local development environment, hacking my way to a point which didn’t blow up and I could visually inspect the output:

#!/usr/bin/env ruby
require 'rubygems'
require 'activeresource'
require 'pp'
class Story < ActiveResource::Base
  self.site = "http://localhost:3000/services/v1/projects/1"
  headers['TOKEN'] = '6cfc2055d1df5605241759014b06b232'
end

p "========================== Stories#create ====================================="
new_story = Story.create(:name => "New Story", :requested_by => "Dan", :description => "Make API ActiveResource compliant")
pp new_story

# etc for all other supported API actions...

However, now that we are doing the real non-spike implementation, we want to automate this end-to-end integration testing as part of our Continuous Integration. That way, we’ll ensure that we are fully ActiveResource-compliant (against current and future versions), and that we don’t have any inadvertent regressions due to future API bugfixes/enhancements.

Digging through the internets and rubyonrails-talk list archives turns up some discussion, but no good answers:

  • Thoughtbot blog post on ActiveResource and Testing
  • rubyonrails-talk post on “Testing XML over HTTP in a Rails app”
  • rubyonrails-talk post on “Testing ActiveResource models with HttpMock”

All of these mention using ActiveResource::HttpMock. However, as Eric and Xavier point out, there seem to be drawbacks to this approach. Plus, even if we get it to work, I’m worried the usage of HttpMock might mask some other issues related to authentication handling, or who knows what else. That’s what real integration tests are for. Finally, HttpMock is an undocumented internal method that seems to exist in order to support Rails’ test suite, so it’s probably not a great idea to depend on that long term.

So, we don’t have a great answer yet, but it seems clear that the highest-value, least-risk approach is to hit a real running app over HTTP with a real ActiveResource client.

The current plan is to leverage our existing Selenium RC test environment, which already has support for spinning up and managing a Rails server with the test environment. We can then port the manual spike tests above to automated ones which run as part of the selenium suite under Continuous Integration, and add appropriate assertions. This isn’t optimal, though, because they won’t actually use Selenium RC at all, which may confuse people. However, there’s no sense reinventing the wheel (and adding time to the overall CI build) by spinning up a separate test server instance when we already spin one up for our selenium suite.

Let us know if you have any clever solutions.

– Chad

  • 0 Shares
  • Share on Facebook
  • Share on Twitter
Adam Milligan

There is no Agile

Adam Milligan
Saturday, January 3, 2009

On occasion, someone will ask me what I do or, more commonly, ask me what Pivotal does. The title on my business card says “Agile Developer,” which nearly inevitably leads to one of a few reactions, almost all of which boil down to this:

“What exactly does Agile mean?”

You know what? It’s a good question. And, after thinking about Agile for several years now, here’s my answer: Agile originally was, and still is, a marketing term.

Allow me to explain.

We’ve been writing software, in one form or another that most people would recognize as such, for some time now. Scott Bain sets the mark at about 30-35 years[1]. Considering the acceleration of technological advancement in the past century, this is a fairly long time.

In the early days, most programs were small, relatively simple, and written by a handful of developers or fewer[2]. So, the general approach to writing them was to put a number of programming language instructions one after another until the computer did the right thing. Eventually.

I believe it was Watts Humphrey who described this type of programming as “ad hoc development.”[3]

Of course, computers got bigger and badder, and software got more complex, and ad hoc quickly stopped scaling. So, people went looking for a new model to stop the hemorrhaging, and they found waterfall. Now, there’s nothing wrong with waterfall for processes that conform to defined process modeling, such as manufacturing Model-Ts, installing sewer lines, or preparing breakfast cereal. But, most people realize these days that it’s not so hot for processes with requirements that may change.

Years passed, and smart people got stuff done by solving different problems in different ways. And, some patterns started to emerge. And, better yet, some truly clever people started to recognize these patterns and write about them and codify them. However, they realized, astutely, that human beings are a hard-headed bunch who don’t like to change their ways. They needed something to shake programmers out of their collective torpor; they needed something flashy to sell; they needed a manifesto. What better thing to offer to an industry plagued by setbacks and missed deadlines than Agile?

To this day the marketing pitch continues; Agile vs. Waterfall, new vs. old, crazy vs. stodgy.

What can we infer from this brief history?

  • The move to waterfall wasn’t motivated by an understanding of the specific challenges of software[4]; it was an attempt to apply a working process, any working process, to an industry that was suffering from far too many failures due to a lack thereof.
  • Many of the principles that have come to be reflected in “agile” processes or “agile” methodologies simply emerged as consistently successful patterns from years of software projects.
  • Over time, a few people have recognized these patterns, studied them, tried to explain them, and expanded upon them to make them even more effective[5].

The conclusion I draw is this: so-called “Agile” is actually nothing more than a collection of good ideas, based on years of collective experience, for improving how we do our jobs as software writers. Or, to put not too fine a point on it, professionalism.

So, if doing “Agile” things means doing your job well, the term ceases to have meaning. As it should. No one should have to sell us good ideas, we should embrace them and have the discipline to stand by them.


[1] Emergent Design: Addison Wesley, 2008

[2] This wasn’t true of all software, of course; the punch card systems that ran the Apollo space program weren’t hacked out by a couple guys in a garage. But, systems like that have their own, mostly time-related, problems.

[3] I loaned out my copy of Winning With Software and can’t immediately find a reference, should anyone care to confirm or deny this credit.

[4] This relates to my rejection of the term “software engineer.” No one really agrees on what writing software is, but we have to call it something. If we call ourselves engineers then we sound like we know what we’re doing. After all, look at all the good stuff in the world that engineers have built.

[5] Pair programming is the classic example of this: code reviews improve code, more frequent code reviews improve code more. How about we code review all code, all the time, as it’s written?

  • 0 Shares
  • Share on Facebook
  • Share on Twitter
Pivotal Labs

Better View Testing with Elementor

Pivotal Labs
Tuesday, November 25, 2008

We’ve got a few mantras at Pivotal. One of them has to do with testing all the time. It’s a Good Thing, for sure. Until recently though, I had always inserted a tacit “except for views” to the end of it. The reason for my reservations wasn’t the fact that view tests can be brittle. Any test can be brittle. I didn’t like testing views because it seemed like the test I was writing never really described the code I was writing. Let’s look at a typical view test to see what I mean:

describe "/posts/index.html.erb" do
  def render_view
    render "/posts/index.html.erb"
    response.body
  end

  before(:each) do
    assigns[:posts] = [
      stub_model(Post, :name => "First!", :body => "first body."),
      stub_model(Post, :name => "Second!", :body => "second body.")
    ]
  end

  describe "assertions using have_tag" do
    it "renders posts" do
      render_view
      response.should have_tag(".post", 2)
    end

    it "renders post headers" do
      render_view
      response.should have_tag(".post .post-name", "First!", 1)
      response.should have_tag(".post .post-name", "Second!", 1)
    end

    it "renders post bodies" do
      render_view
      response.should have_tag(".post .post-body", "first body.", 1)
      response.should have_tag(".post .post-body", "second body.", 1)
    end
  end
end

This snippet uses the have_tag helper. It’s somewhat slow, and to my eyes, expresses intent about as well as an apple floating in a top-hat filled with perfume. The “tag” is a selector? The content filter is just the second argument? And the last argument is the amount of “tag” the response should have? You can test like this, but why would you?

I’ve also seen a more manual pattern, using a library like Hpricot or Nokogiri to parse the response body, then asserting on the results of that:

describe "assertions using Nokogiri" do
  def doc
    @doc ||= Nokogiri(render_view)
  end

  it "renders posts" do
    doc.search('.post').should have(2).nodes
  end

  it "renders post headers" do
    headers = doc.search('.post .post-name')
    headers.should have(2).elements
    headers.detect { |element| element.text == "First!" }.should_not be_nil
    headers.detect { |element| element.text == "Second!" }.should_not be_nil
  end

  it "renders post bodies" do
    bodies = doc.search('.post .post-name')
    bodies.should have(2).elements
    bodies.detect { |element| element.text == "first body." }.should_not be_nil
    bodies.detect { |element| element.text == "second body." }.should_not be_nil
  end
end

It’s faster, since it’s not using have_tag, but still not very expressive. CSS selectors are still littered across the it statements, but at least it’s only once per test. Still, using detect to find content is no good. And I don’t think CSS selectors have any business in it statements at all. That seems like asserting on the name of a method being called, not its behavior.

The solution!

Given my problems with the above approaches, I created a gem that allows the following assertion syntax:

it "renders posts" do
  result.should have(2).posts
end

it "renders post headers" do
  result.should have(2).post_headers
  result.should have(1).post_header.with_text("First!")
  result.should have(1).post_header.with_text("Second!")
end

it "renders post bodies" do
  result.should have(2).post_bodies
  result.should have(1).post_body.with_text("first body.")
  result.should have(1).post_body.with_text("second body.")
end

What’s a result? And how does it know how many posts, post_headers, and post_bodies it has? The result is defined in a before block like so:

require 'elementor'
require 'elementor/spec'

include Elementor

attr_reader :result

before(:each) do
  @result = elements(:from => :render_view) do |tag|
    tag.posts         ".post"
    tag.post_headers  ".post .post-name"
    tag.post_bodies   ".post .post-body"
  end
end

The elements method allows you to name your CSS selectors using the tag block argument. The tag object uses method_missing to register your names. The :from option specifies a method to be called that will return some raw markup.

Naming selectors alone was a huge win for me, but there are a few other cool bits about the @result object. First, you get to use the with_text helper for filtering content. You’ll also get a with_attrs helper for filtering based on a hash of attribute values.

The project is called Elementor, and you can install it like so:

[sudo] gem install elementor

The code is on the GitHub here: github.com/nakajima/elementor (and you can see the CI build here). Take a look at the specs for all of the examples of what you can do. Hopefully, you’ll find it as useful as I have. If not, please share your reasons in the comments!

  • 0 Shares
  • Share on Facebook
  • Share on Twitter

Topics

  • agile (778)
  • rails (113)
  • testing (86)
  • ruby (83)
  • ruby on rails (70)
  • jobs (62)
  • javascript (53)
  • techtalk (44)
  • rspec (38)
  • activerecord (29)
  • productivity (29)
  • gogaruco (29)
  • ironblogger (29)
  • git (28)
  • nyc (27)
  • rubymine (25)
  • mobile (21)
  • cucumber (20)
  • bloggerdome (19)
  • process (19)
  • pivotal tracker (19)
  • design (18)
  • jasmine (18)
  • ios (18)
  • webos (17)
  • objective-c (17)
  • android (16)
  • palm (16)
  • "soft" ware (16)
  • fun (15)
  • tracker ecosystem (15)
  • ci (15)
  • cedar (15)
  • rails3 (14)
  • performance (14)
  • gem (13)
  • bdd (13)
  • selenium (12)
  • css (12)
  • goruco (12)
  • bundler (12)
  • tdd (12)
  • meetup (11)
  • railsconf (11)
  • nyc-standup (11)
  • capybara (10)
  • mac (10)
  • mojo (10)
  • chef (10)
  • rubygems (9)
Subscribe to testing Feed
  1. ←
  2. 1
  3. 2
  4. 3
  5. 4
  6. 5
  7. 6
  8. 7
  9. 8
  10. 9
  11. →
  • About
  • Case Studies
  • Team
  • Community
  • Careers
  • Contact
  • Labs
  • Events

Contact Us

contact@pivotallabs.com
+1 415-77-PIVOT
TwitterLinkedInFacebook

Pivotal Tracker

Tracker is the award-winning agile project management tool that enables real-time collaboration around a shared, prioritized backlog.
Visit pivotaltracker.com >