Sunday, September 09, 2007

Rails: Testing Controllers

Yesterday, Mike Clark posted a blog entry asking "How Would You Test This?" The topic of the entry is how to test controllers. Mike's article is good, and you should start there for context.

I think Mike has the Functional tests well covered in his entry. However, I've never been able to accept that Controllers can only be functionally tested. In my earlier Rails days I would probably have written something similar to the example below and considered it a Unit Test. (Sorry Mike, I prefer Mocha to Flexmock)

require File.dirname(__FILE__) + '/../test_helper'

class MenuItemsControllerTest < Test::Unit::TestCase

def test_create_with_valid_menu_item
controller = MenuItemsController.new
controller.expects(:params).returns(:menu_item => {:title => 'Classic'})
controller.expects(:flash).returns(flash_mock = mock)
controller.expects(:menu_items_url).returns(:menu_items_url)
controller.expects(:redirect_to).with(:menu_items_url)
flash_mock.expects(:[]=).with(:notice, "MenuItem was successfully created.")
controller.create
assert_not_nil controller.instance_eval { @menu_item }
end

def test_create_with_invalid_menu_item
controller = MenuItemsController.new
controller.expects(:params).returns(:menu_item => {})
controller.expects(:render).with(:action => :new)
controller.create
assert_not_nil controller.instance_eval { @menu_item }
end

end

There's a few concerns with the above example. Probably the largest concern is that a lot of mocking almost always results in brittle tests. Another concern is that there's so much noise in the test that it's hard to determine what the intent of the test is. Furthermore, the test is clearly specifying not what should be done, but how it should be done.

On my current project, David Vollbracht created something called an IsolatedControllerTest. The IsolatedControllerTest class mocks the render method and does a few other things to give you the ability to test your controllers in isolation. While David had the right idea, it simply didn't catch on. We have a few IsolatedControllerTests; however, the majority of controller tests are all Functional.

As Mike points out, the current situation often leads to a lack of testing controllers. I'm no more okay with that option than Mike is, but we've had other more interesting problems to solve so I put the thought on a back burner. Luckily, Mike isn't willing to settle.

I think part of the problem is that controllers are not Good Citizens. Controllers violate the first rule of Good Citizenship. Upon creation (Controller.new), controllers are not in a valid state. Instead, controllers depend on being initialized within the framework and having their state set post construction time. That ends up being a problem for unit testing since it requires each test to set additional state on newly created controllers.

Another problem is that methods are marked as protected. For example the *_url and *_path methods are all only accessible from within a controller. I'm sure this was done with the best intentions; however, I subscribe to the philosophy that making something public to increase testability is a good idea.

One of the larger shortcomings that controllers have is that they don't behave like POROs (Plain Old Ruby Objects). Ideally, testing controllers should be as easy as writing the following tests.

require File.dirname(__FILE__) + '/../test_helper'

class MenuItemsControllerTest < Test::Unit::TestCase

def test_redirection_to_menu_items_on_success
controller = MenuItemsController.new(:params => { :menu_item => {:title => 'Classic'} })
controller.expects(:redirect_to).with(controller.menu_items_url)
controller.create
end

def test_flash_notice_on_success
controller = MenuItemsController.new(:params => { :menu_item => {:title => 'Classic'} })
controller.create
assert_equal "MenuItem was successfully created.", controller.flash[:notice]
end

def test_menu_item_is_available_for_view
controller = MenuItemsController.new(:params => { :menu_item => {:title => 'Classic'} })
controller.create
assert_instance_variable(:menu_item).exists_in(controller)
end

end

I look forward to the day when I can create controllers (via Controller.new) and I will have a valid object that I can easily test (with tests similar to the ones above).

5 comments:

  1. Anonymous5:22 PM

    After messing around with Merb ( a rewrite of ActionPack) and seeing why Rails controllers are a bitch to test, its
    becoming clear that ActionPack's days may be numbered. Hopefully Rails 2.0 will introduce a much needed refactoring.

    ReplyDelete
  2. Anonymous5:54 PM

    Jay -- Your last example is obviously leaving out any references to mocking the interaction with the model layer.

    Are you expressing a preference for allowing these tests to hit the DB layer, or are you proposing that the mocking could be automated somehow?

    ReplyDelete
  3. Anonymous6:23 PM

    Brian,

    Dan Manges recently released the UnitRecord gem that redefines Model.new in the unit tests.

    ReplyDelete
  4. Anonymous6:17 PM

    It's not quite that easy, because controllers need a little more context (the whole request, not just params) ...

    But, with Merb, I can get close:

    http://pastie.caboo.se/95898

    ReplyDelete
  5. Minal Jain12:38 AM

    good one !!!
    My functional test facing problem of "ActionController::NonInferrableControllerError: Unable to determine the controller to test from PlayerControllerTest. You'll need to specify it using 'tests YourController' in your test case definition"
    what to do to solve this
    plzz help

    ReplyDelete

Note: Only a member of this blog may post a comment.