helper testing using ActionView::TestCase

Testing testing testing. We test our models, our controllers, and the integration between them. But how often do we pay attention to helpers? Not very often, if the test coverage of my current project's helpers is any indicator.

There are a few tools out there for testing your controllers. There's a plugin called helper_test. ZenTest also provides Test::Rails::HelperTestCase.

There's an even better way, and it's included with Rails itself since at least 2.1: ActionView::TestCase. The name might be misleading, but it's intended for testing view helpers. It also is a kind of underdocumented.

You can skip ahead to the summary if you want the dirty details.

Your very first helper test

The Rails test layout doesn't have an obvious place to put helper tests. I'll take a cue from helper_test, and say to put them in test/unit/helpers. That way, they will be run by rake test:units.

The skeleton test

Let's start out with testing ApplicationController, in test/unit/helper/application_helper_test.rb:

require File.dirname(__FILE__) + '/../../test_helper'
require 'action_view/test_case'
class ApplicationHelperTest < ActionView::TestCase
  def test_nothing
    assert true
  end
end

Some notes:

  • We do a typical require of test/test_helper.rb
  • I'm requiring 'action_view/test_case', because without it, we see an error like uninitialized constant ActionView::TestCase (NameError). I feel like ActiveSupport should find this automatically, but it isn't. You can also toss this in test/test_helper.rb.
  • Inherit ActionView::TestCase
  • Placeholder test

What to test

I want to add a helper for displaying a navigation tab. It will just go to index of a controller you specify. The link should have the 'current' class if you're rendering from controller you are making a tab for.

Based on that, we have two contexts to test:

  • Making a tab for the current controller
  • Making a tab for another controller

Test it first

You'd always be using the helper from a specific controller, so we'll have a parent context of being in a controller, like EventsController.

Now that we have some context, and some specifications, let's translate this into a Shoulda's terminology.

context "creating a tab for 'events'" do
  setup do
    @tab = tab_for('events')
  end

  should "list item with a link to events with 'current' class" do
    assert_dom_equal '<li><a href="/events" class="current">Events</a></li>', @tab
  end
end

context "creating a tab for 'projects'" do
  setup do
    @tab = tab_for('projects')
  end

  should "list item with a link to projects without 'current' class" do
    assert_dom_equal '<li><a href="/projects">Projects</a></li>', @tab
  end
end

The parent context doesn't do anything yet, because we don't know any implementation details about how it's going to determine the current controller. assert_dom_equal, as the name implies, checks that the DOM of the given strings are equivalent. Presumably, this takes care of whitespace, casing, etc.

Let's run it the first time... RED, because we don't have any implementation.

First pass at implementing it

Let's try a first implementation:

module ApplicationHelper
  def tab_for(name)
    url = send("#{name.downcase}_path")
    content_tag :li do
      link_to(name.capitalize, url)
    end
  end
end

Briefly put:

  • We're generate a url. This presumes that the there's a path defined like events_path, as if by map.resource :events.
  • Make a <li>
    • Make a link to the URL we just generated

Let's see how this fares... some GREEN, but still RED. Creating a tab for another controller passes, but creating a tab for the current controller fails. That's because we never determine if we're on on the current controller. How are we going to do that? Here's some fun facts towards enlightenment:

  • Helpers are included in the view.
  • The view exposes controller, which is the rendering controller
  • ActionController::Base exposes controller_name, which has a stringified name of the controller, ie 'events' for EventsController.

I want to check the value of controller.controller_name. We can stub controller to return an actual EventsController using mocha:

context "When on the events controller" do
  setup do
    self.stubs(:controller).returns(EventsController.new)
  end
  # ...
end

And... we have the same failures. No wonder, because we didn't change the implementation. Now to give it another try:

def tab_for(name)
  url = send("#{name.downcase}_path")
  content_tag :li do
    attributes = {}
    attributes[:class] = 'current' if controller.controller_name == name
    link_to(name.capitalize, url, attributes)
  end
end

Wait for it, wait for it... GREEN!

Summary

I believe that wraps things up. Let's just do a quick recap:

  • Stash tests in test/unit/helpers
  • Name your tests like your helper, ie ApplicationHelperTest for ApplicationHelper
  • Inherit from ActionView::TestCase
  • require 'action_view/test_helper' somewhere, probably in test/test_helper.rb
  • Your helper will be included into your test, so you can just use its methods
  • If you need to touch stuff that would normally be included in the view, stub it out
  • Use assert_dom_equals to test the output. Alternative, you might use assert_equal or assert_match

Gotchas

Testing helpers this way is not quite bullet proof though. While coding this up, I encountered a few tough errors that spit out a stack trace because of a nil value somewhere down in the Rails stack. For, I tried using url_for instead of using the named routes, and ActionView::TestCase didn't take too kindly to that.

Shoulda can automatically load custom macros

Rails special sauce: Test::Unit setup and teardown

Use ActionController::TestCase

TextMate Bundle Management

Determine if content_for was used

shoulda 2.0.0, now with gem power

Creating a svn.authorsfile when migrating from subversion to git

Making factory_girl even easier on Rails

Manage your markup with has_markup

Refactoring an ActiveRecord callback

A walk through of test-driven development with shoulda

Getting back to my vim roots with MacVim

Configure git to globally ignore some files

Boston Ruby Group Meeting June 2008 Summary

Software development practices and lessons seen while moving into a new apartment

Embracing your paranoia with acts_as_paranoid

Ebuild Protip: Only fetch from SRC_URI

Ebuild Protip: Use emerge --debug to figure out what's happening

My bad, Planet Gentoo

Ebuild Protip: Know when to NOT define your own src_unpack

Playing with heroku

Managing svn:ignore with impunity

Site updates

Notes from BarCampBoston3

More Gentoo development links than you probably need

Better partials with better-partials

Using a Gentoo Prefixed shell as your login shell

Boston.rb Hackfest 5/07 Post Mortem

Managing RubyGems on Gentoo

Boston.rb Hackfest 4/29 Post Mortem

piston and git for the win

Boston.rb Hackfest 4/15 Post Mortem

How to raise the bar to contribute to your open source project

Dead easy nginx configuration on Gentoo

GitHub: Forking versus being a committer

GitHub: Requesting your changes be pulled from a fork

GitHub: Forking a project

BDD with Shoulda talk from MountainWest RubyConf

Ensuring you get credit where credit is due with Creative Commons

Meet Merb: Ezra's MountainWest RubyConf Talk

Tell Hoe what tests to run

Shoulda macros allows you to embrace your inner slacker

Follow up: Haml no longer be blowing my spot

Haml be blowing my spot

Shoulda and assert{2.0} make swell bedfellows

Hacking on Wikipedia: links for 2008-03-20

Meet Merb: Action methods taking arguments

Meet Merb

Gentoo Prefix as an alternative to MacPorts, Fink

Guide to Gentoo Announcements

More git: links 2008-02-14

Gitosis on Gentoo

git: links for 2008-01-05

Simple caching of markdown markdown in your model

Snowocalypse Boston 12-13-2007

Tracking down Oracle constraint violations with a little SQL and Toad

Focusing a form field at page load with MochiKit

The Big Rewrite is Here

It's been quiet here... I blame Halo 3

NFJS: Thoughts on "OSGi: A Well Kept Secret"

NFJS: Thoughts on "JavaServer Faces: A Whirlwind Tour"

Aspect Oriented Programming: Links for 2007-10-19

Cover your butt with rcov

NFJS: Summary and thoughts on the New England Software Symposium

DRY Controllers and Helpers using Forwardable

Authentication and authorization: Remembering were you came from

RE: dev-ruby/rails: File Collision Free Since September 2007!

Capistrano 2.0.0 on Gentoo at last

dev-ruby/rails: File Collision Free Since September 2007!

Java: Tons Standardized APIs, but not so many standardized practices

Geeks In Boston

Using Markdown in vim

Eating My Own Dogfood: New Site Skin

Up and running with Rails and YUI CSS

Interviewing Research: Links for 08-08-2007

Rails Heavy: Links for 08-07-2007

An Experiment: Links for 08-03-2007

Our evaluation of Selenium for web testing

Pidgin idle time

Of Dreams unfulfilled, JMock, and Expectations

Philadelphia for the rest of the week

Don't Repeat Yourself with with_options

Name your routes, and name them well

Adding a tag listing

Fixing the routes for tags

Rails plugin for Google Analytics and artificial inflation

DevHouseBoston

Technorati

ensuring proper RAILS_ENV in production

That's a lot of tags!!

Annoyances when specifying property values in Spring

Switched templating engine to BlueCloth

First Post!

JRuby on Rails on Gentoo

Aiding and abetting the enemy, rails 1.2.2, and #gentoo-ruby

Update on Xfce 4.4 unmasking

unmask teh Xfce!!!one

XFCE all up in your face

Dealing with libraries that like breaking their APIs

Java gets Groovier

Musings of the current state of open source Java

Java she wanted, Java she got

Summary of Java team meeting

Stale /etc/portage/package.unmask

JAVA_HOME pointing to generation-1 system vm

Java 1.4: Do we still need it?

Tomcat 5.5 and Eclipse 3.2

Updated Java System and Java 1.5 unmasked

Update: The New Java Hotness

The New Java Hotness

Of Java 1.5 and Gentoo

Java ideas for Summer of Code

Eclipse 3.1.2

Hello World