Pivotal Labs

Testing Views by Not Testing Views: Or, The Presenter Pattern

edit Posted by Nick Kallen on Friday October 19, 2007 at 06:40AM

Err The Blog asks: "What's the best way to test views?"

I think the best way to test views is not to test views. Extract all logic from the view into a model or presenter where it can be unit tested. Your views are then mostly declarative and there's minimal need to test them.

Here's an example of the "presenter pattern".

def create_or_destroy_friendship_link(friend)
  if current_user.friends_with?(friend)
    destroy_friendship_link(friend)
  else
    create_friendship_link(friend)
  end
end

You don't need a special class to do a Presenter; a good old-fashioned layer of abstraction will do. The basic idea is to write all conditional+iterative view logic in such a way as to never call a Rails helper directly, or generate any HTML directly, or generate any strings directly. The logic merely delegates to other methods closer to the metal.

Tests then become fairly simple. Write tests of the higher-level conditional/iterative logic in terms of the lower-level methods:

describe FriendshipsHelper, '#create_or_destroy...' do
  it "renders create link when two users are not friends" do
    log_in(users(:bob))
    bob.should_not be_friends_with(users(:amy))
    create_or_destroy_friendship_link.should == create_friendship_link
  end
end

This minimizes the need for view specs. I find in practice that a high percentage of view tests slow development down"”they're implemented not to aid development (since you typically debug views in-browser), but to prevent regression (i.e., they minimize the likelihood of introducing defects later). But since views are one of the most variable parts of a web application, regression tests are of the least value.

As a side note, I love integrate_views -- not because I like to make assertions about the view in my controller tests, but because I hate mocks! I want a controller test to fail if I have a syntax error in my view!

Test-Driven

edit Posted by Alex Chaffee on Thursday October 18, 2007 at 05:58PM

I've revised and extended my old talk on Test-Driven Development. Here it is as a nice PDF, and below the fold as a text outline.

And here are my book references in an Amazon List

CUDdly Models

edit Posted by Nick Kallen on Tuesday October 16, 2007 at 05:47AM

Teddy Bear

Better Rails Code through

...ActiveRecords with no public methods that have side-effects--other than Create, Update, and Destroy (CUD).

CUDly Models

In a typical web application, some "triggered actions" or "side-effects" occur in response to various events. Examples: A confirmation email is sent when a User registers. A ping is sent to Technorati when a BlogArticle is published. Cookies are set when a user logs in. Two Friendship objects are created between two users when one approves the other's FriendshipRequest.

Often these side-effects are modeled with public methods on an ActiveRecord. For instance,

class FriendshipRequest < ActiveRecord::Base
  def accept!
    self.status = ACCEPTED
    save!
    Friendship.create!(:from => from, :to => to)
    Friendship.create!(:to => from, :from => to)
  end
end

This is code is dangerous, as shown below. The principal of CUDly models is eliminate all public methods on your ActiveRecord that have any side effects. CUDly models are much safer. Let's replace the dangerous code with the equivalent, more friendly, CUDly code:

class FriendshipRequest
  after_update :create_mutual_friendship

  private
  def send_email_on_accept
    if status == ACCEPTED
      Friendship.create!(:from => from, :to => to)
      Friendship.create!(:to => from, :from => to)
    end
  end
end

The above CUDly code exploits the richness of ActiveRecord's lifecycle callbacks to trigger the side-effect. This illustrates a general principle: in a perfectly CUDly world, constrain the interface to your models such that no methods have side effects other than Create, Update, and Destroy.

There are several virtues to CUDly models: encapsulation, transactions, consistent interface, and lifecycle power.

Encapsulation

Your side-effects can be hidden behind the standard ActiveRecord interface. And your Controllers will be skinny as can be! Compare:

class FriendshipRequestsController
  def update
    friendship_request = FriendshipRequest.find(params[:id])
    if params[:friendship_request][:state] == ACCEPTED
      friendship_request.accept!
    else
      friendship_request.reject!
    end
  end
end

Instead, you can write an equivalent, Formulaic Controller:

class FriendshipRequestsController
  def update
    friendship_request = FriendshipRequest.find(params[:id])
    friendship_request.attributes = params[:friendship_request]
    friendship_request.save
  end
end

Transactions

Thanks to ActiveRecord, the CUDly approach has rich transactional semantics. In the CUDly implementation, if any of the Friendship.create! invocations fails, the entire transaction is rolled back, meaning the you cannot put the world in an incoherent state (where Tom and Dick are only half-friends). The equivalent non-CUDly code is the onerous and obese:

class FriendshipRequest < ActiveRecord::Base
  def accept!
    self.status = ACCEPTED
    self.class.transaction do
      save!
      Friendship.create!(:from => from, :to => to)
      Friendship.create!(:to => from, :from => to)
    end
  end
end

Who wants to cuddle with code like that?!

A Rich Consistent Interface

ActiveRecord already has a wonderful pattern: build, then test. It's so simple, and yet so powerful. Why not re-use it?

f = Friendship.new
if f.save ...

#Save returns true or false, and it sets errors on the model to be displayed by the user. Using CUDly code, you continue to do that:

class FriendshipRequestsController
  def update
    friendship_request = FriendshipRequest.find(params[:id])
    friendship_request.attributes = params[:friendship_request]
    if friendship_request.save        
      flash[:notice] = ...
    else
      flash[:error] = ...
      render :action => :edit
    end
  end
end

Try doing that with unCUDly #accept and #reject methods! Surely it will be unCUDly and ungodly!

Leverage the Power of the Lifecycle

A fundamental limitation of public, side-effecting methods is that they can be called at any time for any reason. Suppose we had something like this:

class MyModel < ...
  def foo=(bar)
    send_an_email!
  end

This could be called as part of #attributes=, triggering the email deilvery regardless of whether the model was valid and could be saved! In the CUDly implementation, on the other hand:

class MyModel
  after_save :send_an_email
  def send_an_email
  ...

Thanks to the flexibility of #before_validation_on_create, #before_create, #after_update, #after_initialize, #after_find, etc., you can ensure that your triggered action only happens after successful validation, or regardless of validation, or only on update, or only on destroy--you name it! Try enforcing that with a public method!

Make Your Code a CUDly Code

There is a beautiful symmetry in having all side-effecting methods "funneled" through the three "dangerous" methods (create, update, and destroy). It appeals to my sense of elegance and order. I've used this design strategy 100% for the last few months and it's been a smashing success! It truly is the way ActiveRecord was meant to be used. So give it a try!

dot.rake

edit Posted by Alex Chaffee on Monday October 15, 2007 at 12:29PM

[Update: 10/15/07 - incorporated changes by David Vrensk (and a few more from me). Now it merges in associations into the arc, and also deals with inheritance (e.g. STI).]

While googling for articles on Rails associations, I happened upon this gem of a script by Matt Biddulph. I loved it so much I made it a rake task! Once you install GraphViz like this:

sudo port install graphviz

and put dot.rake in your lib/tasks directory, then running this:

rake dot

produces diagrams like this:

BBC Programme Catalogue codebase

And you can also import the DOT source into OmniGraffle for further editing, like this:

open -a "OmniGraffle" model.dot

Cacheable Flash 0.1.4 -- Test Helpers

edit Posted by Brian Takita on Sunday October 07, 2007 at 09:45PM

I just released Cacheable Flash 0.1.4. This version includes test helpers so you can easily test your cache messages. It works by allowing you to make assertions on the flash cookie.

Here is a test/unit example:

  require "cacheable_flash/test_helpers"

  class TestController < ActionController::Base
    def index
      flash["notice"] = "In index"
    end
  end

  class ControllerTest < Test::Unit::TestCase
    include CacheableFlash::TestHelpers

    def setup
      @controller = TestController.new
      @request = ActionController::TestRequest.new
      @response = ActionController::TestResponse.new
    end

    def test_cacheable_flash_action
      get :index
      asset_equal "In index", flash_cookie["notice"]
    end
  end

Here is a rspec example:

  require "cacheable_flash/test_helpers"

  class TestController < ActionController::Base
    def index
      flash["notice"] = "In index"
    end
  end

  describe TestController, "#index" do
    include CacheableFlash::TestHelpers

    it "writes to the flash cookie" do
      get :index
      flash_cookie["notice"].should == "In index"
    end
  end

You can install Cacheable Flash by running:

ruby script/plugin install svn://rubyforge.org/var/svn/pivotalrb/cacheable_flash/trunk

See the Cacheable Flash blog post, Show Flash Messages on Cached Pages, and the README for more information.

sub

edit Posted by Alex Chaffee on Tuesday October 02, 2007 at 03:06PM

Yellow Submarine

(Update -- version 0.3 released 2-Oct-07. Release notes are here.)

We use subversion for our source control. We love it. But we've noticed a few flaws, and a few weeks ago I decided I'd had enough and wrote a wrapper for it that fixes a few of the most glaring ones:

  • Externals get messed up pretty frequently. If you remove or rename an external, the old one gets left around on disk, and if you convert an external to a "real" directory or vice versa then the next update simply fails.
  • Externals are updated in series, not in parallel, meaning that if you have a lot of externals your updates can take an excruciatingly long time.
  • Externals are updated even if they're frozen to a specific revision number, which wastes even more time on update.
  • If you want a clean checkout -- say, for an automated build -- the only way to do it is to do a full checkout, even if 99% of the files are already there on disk.
  • The co command is not compatible with the convention of putting files under /trunk, requiring you to type out your whole repository URL followed by /foo/trunk foo
  • The name of the executable is hard to pronounce -- either "ess vee enn" or "seven", but nobody says "seven" except when they're saying "seven up", which is, I admit, a pretty good pun, but come on, how much cooler is it to say, "sub"?

The current version of sub fixes all of the above (except for converting directories to and from externals, and I'm going to make that work pretty soon).

Install with

sudo gem install sub

Help text is below the fold.