how to write and inherit from an abstract subclass

2019-04-10 00:20发布

问题:

I have a bunch of Rails 3.1 controllers which all have very similar testing requirements. I have extracted out the common code (all Test::Unit style), e.g. the following three tests are completely reusable across all of them:

  def create
    new_record    = { field_to_update => new_value }
    create_params = { :commit => "Create", :record => new_record }
    post :create, create_params
  end

  test "should_not_create_without_login" do
    assert_no_difference(count_code) do create; end
    assert_need_to_log_in
  end

  test "should_not_create_without_admin_login" do
    login_as_non_admin
    assert_no_difference(count_code) do create; end
    assert_needs_admin_login
  end

  test "should_create" do
    login_as_admin
    assert_difference(count_code) do create; end
    assert_redirected_to list_path
  end

and I intended that it could go in an abstract class which inherits from ActionController::TestCase. Then each functional test would only need to override the abstract methods, ending up pleasingly small and clean, e.g.

class Admin::AvailabilitiesControllerTest < Admin::StandardControllerTest
  tests Admin::AvailabilitiesController

  def model          ; Availability              end
  def id_to_change   ; availabilities(:maybe).id end
  def field_to_update; :value                    end
  def new_value      ; 'maybe2'                  end
  def list_path      ; admin_availabilities_path end
end

However, when I try this, it appears that the framework tries to run the test methods directly from the abstract class, rather than from the inherited class:

E                                                           
===================================================================================================
Error:                                                      
test_should_not_create_without_login(Admin::ControllerTestBase):
NoMethodError: undefined method `model' for test_should_not_create_without_login(Admin::ControllerTestBase):Admin::ControllerTestBase
    test/lib/admin_controller_test_base.rb:7:in `count_code'
    test/lib/admin_controller_test_base.rb:68:in `block in <class:ControllerTestBase>'
===================================================================================================

I've heard that other testing frameworks and gems can provide mechanisms for meta-programming of tests, so maybe I'm going about this in entirely the wrong way. But I've tried several things and looked at RSpec, coulda, shoulda, context, contest ... and I still can't see a way to achieve what I'm after. Any ideas? Thanks!

回答1:

I finally figured this out - once I realised that this is a general Ruby Test::Unit issue rather a Rails testing issue, a quick google instantly revealed How do I inherit abstract unit tests in Ruby? which already had a good answer. Then the only missing piece was being able to use the syntactic sugar:

test "something should behave in a certain way" do
    ...
end

rather than

def test_something_should_behave_in_a_certain_way" do
    ...
end

I found the answer to this within the ActiveSupport codebase itself, under lib/active_support/test_case.rb:

extend ActiveSupport::Testing::Declarative

This module defines test as a class method (which is why extend is required rather than include).

So the complete solution looks like this:

# tests/functional/admin/availabilities_controller_test.rb

class Admin::AvailabilitiesControllerTest < ActionController::TestCase
  tests Admin::AvailabilitiesController
  include Admin::ControllerTests

  # non-reusable tests and helper methods specific to this 
  # controller test go here
end


# lib/admin/controller_tests.rb

module Admin::ControllerTests
  extend ActiveSupport::Testing::Declarative

  test "this test can be reused by anything which includes this module" do
    ...
  end
end

The downside of this module-based approach is that you can't override the included tests. I guess that's just a fundamental limitation of Test::Unit - maybe the best answer is to move to RSpec, but I don't know it well enough yet to be sure.