133

Given that I have a Personable concern in my Rails 4 application which has a full_name method, how would I go about testing this using RSpec?

concerns/personable.rb

module Personable
  extend ActiveSupport::Concern

  def full_name
    "#{first_name} #{last_name}"
  end
end
yas4891
  • 4,774
  • 3
  • 34
  • 55
Kyle Decot
  • 20,715
  • 39
  • 142
  • 263
  • What testing framework are you using? Also remember Personable is just a normal Ruby module. Test it just like you would test any other mixin. – Lee Jarvis May 13 '13 at 15:05
  • Hasn't `ActiveSupport::Concern` been taken out of Rails? I thought it went a little while ago. – Russell May 13 '13 at 15:06
  • @LeeJarvis I'm using Rspec along w/ FactoryGirl – Kyle Decot May 13 '13 at 15:08
  • Ah, google informs me it was only the `InstanceMethods` module inside `Concern` that was removed. I still think concerns are bad design though. What's wrong with service objects? – Russell May 13 '13 at 15:09
  • 2
    @KyleDecot http://stackoverflow.com/questions/1542945/testing-modules-in-rspec http://stackoverflow.com/questions/16453266/rails-rspec-testing-concerns-class-methods http://benediktdeicke.com/2013/01/custom-rspec-example-groups/ these should help – Lee Jarvis May 13 '13 at 15:09
  • @Russell Rails 4 actively promotes concerns (I also agree service objects are a better idea) but unfortunately that's not how Rails core sees it. People just need to remember they're just normal Ruby modules and should be used appropriately. – Lee Jarvis May 13 '13 at 15:10
  • @LeeJarvis By "Rails core" I assume you mean "DHH". It's definitely not a good idea to do something just because "Rails core" says to do it that way. – Russell May 13 '13 at 15:14
  • 4
    @Russell I agree. That said, I wouldn't not help someone with their questions just because they were following a Rails-y way of doing something that I didn't agree with it. Anyway this is kinda of escaping the subject of this question :-) – Lee Jarvis May 13 '13 at 15:16
  • :-) Agree. Sorry Kyle! – Russell May 13 '13 at 15:18
  • Not a problem. Always good to get other developers opinions on the subject I'm currently dealing w/ – Kyle Decot May 13 '13 at 15:25

5 Answers5

218

The method you found will certainly work to test a little bit of functionality but seems pretty fragile—your dummy class (actually just a Struct in your solution) may or may not behave like a real class that includes your concern. Additionally if you're trying to test model concerns, you won't be able to do things like test the validity of objects or invoke ActiveRecord callbacks unless you set up the database accordingly (because your dummy class won't have a database table backing it). Moreover, you'll want to not only test the concern but also test the concern's behavior inside your model specs.

So why not kill two birds with one stone? By using RSpec's shared example groups, you can test your concerns against the actual classes that use them (e.g., models) and you'll be able to test them everywhere they're used. And you only have to write the tests once and then just include them in any model spec that uses your concern. In your case, this might look something like this:

# app/models/concerns/personable.rb
module Personable
  extend ActiveSupport::Concern

  def full_name
    "#{first_name} #{last_name}"
  end
end

# spec/concerns/personable_spec.rb
require 'spec_helper'

shared_examples_for "personable" do
  let(:model) { described_class } # the class that includes the concern

  it "has a full name" do
    person = FactoryBot.build(model.to_s.underscore.to_sym, first_name: "Stewart", last_name: "Home")
    expect(person.full_name).to eq("Stewart Home")
  end
end

# spec/models/master_spec.rb
require 'spec_helper'
require Rails.root.join "spec/concerns/personable_spec.rb"

describe Master do
  it_behaves_like "personable"
end

# spec/models/apprentice_spec.rb
require 'spec_helper'

describe Apprentice do
  it_behaves_like "personable"
end

The advantages of this approach become even more obvious when you start doing things in your concern like invoking AR callbacks, where anything less than an AR object just won't do.

Rimian
  • 36,864
  • 16
  • 117
  • 117
Josh Leitzel
  • 15,089
  • 13
  • 59
  • 76
  • 2
    One disadvantage of this is that it will slow down `parallel_tests`. I think it will be better to have separate tests instead of using `shared_examples_for` and `it_behaves_like`. – Artem Kalinchuk Jan 21 '14 at 12:23
  • 9
    @ArtemKalinchuk I'm not sure that's true, per https://github.com/grosser/parallel_tests/issues/168 `parallel_tests` are based per file, so shared examples should not slow it down. I would also argue that properly grouped shared behaviors, trumps testing speed. – Aaron K Apr 09 '14 at 22:16
  • 8
    Make sure to include the `concerns` directory in your `spec_helper.rb` https://github.com/rspec/rspec-core/issues/407#issuecomment-1409871 – Ziggy Jun 06 '14 at 00:20
  • 2
    I couldn't find anything about including the concerns directory in that link. Could you please clarify how this is done? I can't get my RSpec test to recognize the module in one of my concerns. – Jake Smith Dec 28 '14 at 02:08
  • @JakeSmith see https://www.relishapp.com/rspec/rspec-core/docs/example-groups/shared-examples – Pak Jan 13 '15 at 16:07
  • The only thing that caught me out here was that I needed to instantiate my model, rather than just using the class. `let(:model) { described_class }` needed to be `let(:model) { described_class.new }` to avoid a nasty ```NoMethodError: undefined method `reflect_on_association' for Class:Class```. – Steve Hill Jul 02 '15 at 15:23
  • 5
    Do not add `_spec` to the filename which contains shared_examples_for (personable_spec.rb in this case), otherwise you will get a misleading warning message - https://github.com/rspec/rspec-core/issues/828. – Lalu Mar 11 '16 at 16:18
  • it's very very helpfull for me! – Sergio Belevskij Mar 24 '17 at 16:48
  • You could probably .build your instance instead of .create and turbocharge your tests. – David Hempy Feb 21 '19 at 16:49
  • `require "concerns/personable_spec"` is enough. The spec folder is in the load path. – Piioo Mar 25 '20 at 08:40
76

In response to the comments I've received, here's what I've ended up doing (if anyone has improvements please feel free to post them):

spec/concerns/personable_spec.rb

require 'spec_helper'

describe Personable do
  let(:test_class) { Struct.new(:first_name, :last_name) { include Personable } }
  let(:personable) { test_class.new("Stewart", "Home") }

  it "has a full_name" do
    expect(personable.full_name).to eq("#{personable.first_name} #{personable.last_name}")
  end
end
Kyle Decot
  • 20,715
  • 39
  • 142
  • 263
8

Another thought is to use the with_model gem to test things like this. I was looking to test a concern myself and had seen the pg_search gem doing this. It seems a lot better than testing on individual models, since those might change, and it's nice to define the things you're going to need in your spec.

lobati
  • 9,284
  • 5
  • 40
  • 61
2

The following worked for me. In my case my concern was calling generated *_path methods and the others approaches didn't seem to work. This approach will give you access to some of the methods only available in the context of a controller.

Concern:

module MyConcern
  extend ActiveSupport::Concern

  def foo
    ...
  end
end

Spec:

require 'rails_helper'

class MyConcernFakeController < ApplicationController
  include MyConcernFakeController
end

RSpec.describe MyConcernFakeController, type: :controller do    
  context 'foo' do
    it '' do
      expect(subject.foo).to eq(...)
    end
  end
end
Nathan
  • 222
  • 2
  • 9
  • if the concern is defined in `controllers/concerns/my_concern.rb`, where is your concern spec? `spec/controllers/concerns/my_concern_spec.rb`, or `spec/controllers/my_concern_fake_controller_spec.rb`? – richardsonwtr Feb 24 '22 at 22:37
  • 1
    I think that's a matter of preference, but in my case I wanted to test the concern, not the controller (but yes, they're connected) so your first suggestion: `spec/controllers/concerns/my_concern_spec.rb` – Nathan Mar 01 '22 at 07:33
-3

just include your concern in spec and test it if it returns the right value.

RSpec.describe Personable do
  include Personable

  context 'test' do
    let!(:person) { create(:person) }

    it 'should match' do
       expect(person.full_name).to eql 'David King'
    end
  end
end
Rimian
  • 36,864
  • 16
  • 117
  • 117
Jin Lim
  • 1,759
  • 20
  • 24