Automatically creating testcases

written by benjamin on December 21st, 2006 @ 11:58 AM

One of the great features of ruby (and rails) is the ability to automatically create code. A lot of our controller code is generated automatically by some generic functions. For example to add our ‘edit-abstract’ functionality for movies, we just need to add a

set_abstract_for :movie

to our controller. While it took us some time to genericize our controller code, we now have less LoC, an investment, that will surely pay off. While rewriting some of our controller methods, we realized that we try hard to keep our controller DRY, while we repeat ourselves over and over again when writing test cases.

The solution to this is to write helpers for testcases, just like writing helpers for the controllers. We’ve added a test_omdb_helper.rb to the test/ directory and required it in the standard ‘test_helper.rb’ file. One of our first test-case-generators was the view_test_for. This helper method might be of interest to someone out there as it is not OMDB-related. Its purpose is simply to test all of our views. OMDB has a lot of different views but not all of our views have special functionalities. For example, the statistic views for a category or the filmography of a person just displays simple data. Our goal was to be able to test every single view without writing something like that all the time:

def test_movie_index
  get :index, :id => movies(:king_kong).id
  assert_response :success
end

So we started with a small method, implementing exactly the logic above:

def view_test_for( klass, id, opts = {} )
  define_method( "test_view_#{options[:action]}_#{id.to_s}" ) do
    # another way of saying: movies(:king_kong) :-)
    object = self.send(klass.to_s.pluralize, id)
    get options[:action], :id => object.id
    assert_response :success
  end
end

Now we can test all of our views with just a few lines in our movie_controller_test.rb:

view_test_for :movie, :king_kong, :action => :index
view_test_for :movie, :king_kong, :action => :cast
view_test_for :movie, :king_kong, :action => :review

This was a good start of the basic views but it has his flaws in flexiblity, e.g. if you want to do more than just know that it does not throw any exception. One small extension added to the method above will allow you to add custom code to each of the view_tests.

def view_test_for( klass, id, opts = {} )
  define_method( "test_view_#{options[:action]}_#{id.to_s}" ) do
    object = self.send(klass.to_s.pluralize, id)
    get options[:action], :id => object.id
    assert_response :success
    yield self if block_given?
  end
end

I do not know about your experience with ruby, at least i needed some time to feel comfortable with blocks and yields, but now its great to work with them. Lucas Carlson and Leonard Richardson write in their Ruby Cookbook (which I highly recommend):

The yield keyword acts like a special method, a stand-in for whatever code block was passed in. [...] This may seem mysterious if you’re unfamiliar with the practice of passing blocks around, but it is usually the preferred method of calling blocks in Ruby.

You should definitely take a closer look to yield, if you’re not familiar with it. We can now pass in a few lines of code, that will be evalutated inside the test, for example checking for a certain tag:

view_test_for :movie, :king_kong, :action => :cast do |test_case|
  test_case.assert_tag :tag => :a,
      :attributes => { :href => "http://test.host/person/" +
                        instance.people(:peter_jackson).id.to_s }
end

Now that helps a lot to write more complex tests. Actually we found a lot of options, we wanted to pass in to our view-test. Here’s another example

view_test_for :movie, :king_kong, :action => :info, :template => 'info.rhtml',
                                  :xhr => true, :method =>:post do |test_case|
  test_case.assert_tag :tag => :a, :content => instance.people(:peter_jackson).name
end

Here’s the whole test_view_for-method, one of the options is OMDB-related, you might want to remove this option. We placed most of our templates in a common/ directory. So we added a test for the template name, if it uses a common template or one of the controller-view-directories (e.g. view/movie vs. view/common, that is).

def view_test_for( klass, id, opts = {} )
  options = { :action       => :index, 
    :template     => 'overview.rhtml',
    :common_template => false,
    :method       => :get,
    :fetch_method => klass.to_s.pluralize,
    :xhr          => false, 
    :response     => :success,
    :login        => nil }
  options.update(opts)
  define_method( "test_view_#{options[:action]}_#{id.to_s}" ) do
    object = self.send(options[:fetch_method], id)
    login_as options[:login] unless options[:login].nil?
    if options[:xhr] 
      xhr options[:method], options[:action], :id => object.id
    else
      self.send( options[:method], options[:action], :id => object.id )
    end
    assert_response options[:response]
    assert_template options[:template] if options[:response] == :success
    assert @response.rendered_file.include?("views/common/") if options[:common_template]
    yield self if block_given?
  end
end

Comments

  • Alain Ravet on 21 Dec 14:41

    Ben, When a test fails, isn't it too hard to locate and understand the error?
  • Ben on 21 Dec 20:19

    Alain, well, we did not have any problem yet, we mainly use these tests to check all of our templates, as most of our templates get included on various occasions. So most of the time we just get template or helper-errors. And they're not hard to track down. More complex functional-tests get their own test-cases.

Post a comment

Options:

Size