voxobscuro
voxobscuro

Reputation: 2238

Testing ruby superclass inherited functionality with rspec

Given that I have an abstract class which provides inherited functionality to subclasses:

class Superclass
  class_attribute :_configuration_parameter

  def self.configuration_parameter config
    self._configuration_parameter = config
  end

  def results
    unless @queried
      execute
      @queried = true
    end

    @results
  end

  private

  # Execute uses the class instance config
  def execute
    @rows = DataSource.fetch self.class._configuration_parameter
    @results = Results.new @rows, count
    post_process
  end

  def post_process
    @results.each do |row|
      # mutate results
    end
  end
end

Which might be used by a subclass like this:

class Subclass < Superclass
  configuration_parameter :foo

  def subclass_method
  end
end

I'm having a hard time writing RSpec to test the inherited and configured functionality without abusing the global namespace:

RSpec.describe Superclass do
  let(:config_parameter) { :bar }

  let(:test_subclass) do
    # this feels like an anti-pattern, but the Class.new block scope
    # doesn't contain config_parameter from the Rspec describe

    $config_parameter = config_parameter

    Class.new(Superclass) do
      configuration_parameter $config_parameter
    end
  end

  let(:test_instance) do
    test_subclass.new
  end

  describe 'config parameter' do
    it 'sets the class attribute' do
      expect(test_subclass._configuration_parameter).to be(config_parameter)
    end
  end

  describe 'execute' do
    it 'fetches the data from the right place' do
      expect(DataSource).to receive(:fetch).with(config_parameter)
      instance.results
    end
  end
end

The real world superclass I'm mocking here has a few more configuration parameters and several other pieces of functionality which test reasonably well with this pattern.

Am I missing something obviously bad about the class or test design?

Thanks

Upvotes: 1

Views: 2075

Answers (1)

Chris Salzberg
Chris Salzberg

Reputation: 27374

I'm just going to jump to the most concrete part of your question, about how to avoid using a global variable to pass a local parameter to the dummy class instantiated in your spec.

Here's your spec code:

let(:test_subclass) do
  # this feels like an anti-pattern, but the Class.new block scope
  # doesn't contain config_parameter from the Rspec describe

  $config_parameter = config_parameter

  Class.new(Superclass) do
    configuration_parameter $config_parameter
  end
end

If you take the value returned from Class.new you can call configuration_parameter on that with the local value and avoid the global. Using tap does this with only a minor change to your existing code:

let(:test_subclass) do
  Class.new(SuperClass).tap do |klass|
    klass.configuration_parameter config_parameter
  end
end

As to the more general question of how to test functionality inherited from a superclass, I think the general approach of creating a stub subclass and writing specs for that subclass is fine. I personally would make your _configuration_parameter class attribute private, and rather than testing that the configuration_parameter method actually sets the value, I'd instead focus on checking that the value is different from the superclass value. But I'm not sure that's in the scope of this question.

Upvotes: 2

Related Questions