james00794
james00794

Reputation: 1147

Dynamic generation of tests for an ActiveSupport::Concern

I have a Concern defined like this:

module Shared::Injectable 
  extend ActiveSupport::Concern

  module ClassMethods
    def injectable_attributes(attributes)
      attributes.each do |atr|
        define_method "injected_#{atr}" do
           ...
        end
      end
    end
  end

and a variety of models that use the concern like this:

Class MyThing < ActiveRecord::Base
  include Shared::Injectable
  ...
  injectable_attributes [:attr1, :attr2, :attr3, ...]
  ...
end

This works as intended, and generates a set of new methods that I can call on an instance of the class:

my_thing_instance.injected_attr1
my_thing_instance.injected_attr2
my_thing_instance.injected_attr3

My issue comes when I am trying to test the concern. I want to avoid manually creating the tests for every model that uses the concern, since the generated functions all do the same thing. Instead, I thought I could use rspec's shared_example_for and write the tests once, and then just run the tests in the necessary models using rspec's it_should_behave_like. This works nicely, but I am having issues accessing the parameters that I have passed in to the injectable_attributes function.

Currently, I am doing it like this within the shared spec:

shared_examples_for "injectable" do |item|
  ...
  describe "some tests" do
    attrs = item.methods.select{|m| m.to_s.include?("injected") and m.to_s.include?("published")}
    attrs.each do |a|
      it "should do something with #{a}" do
        ...
      end
    end
  end
end

This works, but is obviously a horrible way to do this. Is there an easy way to access only the values passed in to the injectable_attributes function, either through an instance of the class or through the class itself, rather than looking at the methods already defined on the class instance?

Upvotes: 0

Views: 100

Answers (2)

Paul Fioravanti
Paul Fioravanti

Reputation: 16793

Since you say that you "want to avoid manually creating the tests for every model that uses the concern, since the generated functions all do the same thing", how about a spec that tests the module in isolation?

module Shared
  module Injectable
    extend ActiveSupport::Concern

    module ClassMethods
      def injectable_attributes(attributes)
        attributes.each do |atr|
          define_method "injected_#{atr}" do
            # method content
          end
        end
      end
    end
  end
end

RSpec.describe Shared::Injectable do
  let(:injectable) do
    Class.new do
      include Shared::Injectable

      injectable_attributes [:foo, :bar]
    end.new
  end

  it 'creates an injected_* method for each injectable attribute' do
    expect(injectable).to respond_to(:injected_foo)
    expect(injectable).to respond_to(:injected_bar)
  end
end

Then, as an option, if you wanted to write a general spec to test whether an object actually has injectable attributes or not without repeating what you've got in the module spec, you could add something like the following to your MyThing spec file:

RSpec.describe MyThing do
  let(:my_thing) { MyThing.new }

  it 'has injectable attributes' do
    expect(my_thing).to be_kind_of(Shared::Injectable)
  end
end

Upvotes: 1

Jake Kaad
Jake Kaad

Reputation: 103

What about trying something like this:

class MyModel < ActiveRecord::Base
  MODEL_ATTRIBUTES = [:attr1, :attr2, :attr3, ...]
end

it_behaves_like "injectable" do 
  let(:model_attributes) { MyModel::MODEL_ATTRIBUTES }
end

shared_examples "injectable" do
  it "should validate all model attributes" do 
    model_attributes.each do |attr|
      expect(subject.send("injected_#{attr}".to_sym)).to eq (SOMETHING IT SHOULD EQUAL)
    end
  end
end

It doesn't create individual test cases for each attribute, but they should all have an assertion for each attribute. This might at least give you something to work from.

Upvotes: 0

Related Questions