How to Test a Concern in Rails - ruby-on-rails

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

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.

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

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.

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

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

Related

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?
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
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.

Test (rspec) a module inside a class (Ruby, Rails)

I have this to work with...
class LegoFactory # file_num_one.rb
include Butter # this is where the module is included that I want to test.
include SomthingElse
include Jelly
def initialize(for_nothing)
#something = for_nothing
end
end
class LegoFactory # file_num_2.rb
module Butter
def find_me
# test me!
end
end
end
So, when LegoFactory.new("hello") we get the find_me method as an instance method of the instantiated LegoFactory.
However, there are quite a few modules includes in the class and I just want to separate the Butter module without instantiating the LegoFactory class.
I want to test ONLY the Butter module inside of the LegoFactory. Names are made up for this example.
Can this be done?
Note: I cannot restructure the code base, I have to work with what I have. I want to just test the individual module without the complexity of the rest of the LegoFactory class and its other included modules.
A way to do it is to create a fake class that includes your module in order to test it:
describe LegoFactory::Butter do
let(:fake_lego_factory) do
Class.new do
include LegoFactory::Butter
end
end
subject { fake_lego_factory.new }
describe '#find_me' do
it 'finds me' do
expect(subject.find_me).to eq :me
end
end
end
You can also implement in the fake class a mocked version of any method required by find_me.

How to include methods of models in RSpec test cases

I'm writing some Rspec tests with capybara , and as part of that I need some methods of model.
I have created my model as:
class MyModel < ActiveRecord::Base
def method_name
#some stuff..
end
end
Now, I want to use MyModel in my Rspec test cases.
I tried to includeconfig.include Models in spec_helper.rb but it throws error
Uninitialized constant Models
And when I tried to include
include MyModel.new.method_name()
it throws error `include': wrong argument type nil (expected Module) (TypeError)
Without including any model class it runs test cases, but then my test cases are useless.
Here is my Rspec test case
require 'spec_helper'
describe "State Agency Page" do
let(:state_data) { FactoryGirl.build(:state_data) }
require 'mymodel.rb'
before {visit state_modifier_path}
it "should have breadcrumb", :js=>true do
page.should have_css('.breadcrumb')
end
end
Please provide any solution.
Thanks in advance.
I don't know what you mean by "your test cases are useless", but you seem to misunderstand the role of Ruby's include method.
With Rails, or the use of the rspec-rails gem with RSpec, your classes will be autoloaded when you reference the corresponding class constant (e.g. MyModel). So there generally is no need to do manual "loading" of individual models. Just make sure you have require 'spec_helper' at the beginning of your specs.
As for the errors you were getting with your attempts to use include, I suggest you find and read a Ruby reference to understand the semantics of the include method and why each attempt of yours failed in the way it did.

RSpec page variable

How is it that rspec feature tests implicitly know to use methods such as find, within, and fill_in from the page object?
I've written a helper class for some of my rspec tests and wanted to use those methods, and realized that I needed to pass the page object into the method, and then use page.find and the like.
RSpec achieves this by including Capybara::DSL in those cases where it wants those methods available. The module is pretty elegant, if you want to take a look at https://github.com/jnicklas/capybara/blob/f83edc2a515a3a4fd80eef090734d14de76580d3/lib/capybara/dsl.rb
suppose you want to include the following module:
module MailerMacros
def last_email
ActionMailer::Base.deliveries.last
end
def reset_email
ActionMailer::Base.deliveries = []
end
end
to include them, just call config.include(MailerMacros), like this:
RSpec.configure do |config|
config.include(MailerMacros)
end
now, you should be able to call reset_email() & last_email instead of MailerMacros::reset_email().

RSpec: how can I mixin a module into RSpec test?

In my app when user share something he's rating grows. When he tries to share something twice – he will get no additional rating for second try. For application, share callback is triggered by client-side with JS, so, it's just a regular GET-request. So, I need to test this functionality. It's easy. But I'v got several sections with this behavior. Every controller from that sections have method named "rating_from_share", so tests are pretty similar. I think it is good idea to extract that test's in a mixing and include them where it should be, but I can't figure out, how can I do this.
So, is it real to include a mixing with RSpec to a RSpec test? Maybe something kind of metaprogramming can solve this problem?
P.S. realization of "rating_from_share" method is not really the same but only the output result, so I can't to aggregate it to a superclass and test them here.
EDIT:
According to Vimsha answer, should I do something like this?
Module Share
def share
it 'should be fun'
expect(#fun.isFun?).toBe == 'yup' # the #fun is declared in ShareTest
end
end
end
describe "Share Test" do
extend Share
before :each do
#fun = Fun.new
end
it 'should do test' do
share # call method from Share module, which has real RSpec code?
end
end
The code is written just here, I'm just trying to get the idea.
A common practice in RSpec is to store such logic under spec/support. For instance:
# spec/support/ratings_macros.rb
module RatingsMacros
...
end
You then need to load it from your spec_helper:
# spec/spec_helper.rb
...
RSpec.configure do |config|
...
config.include RatingsMacros
You can now call in your tests all the methods defined in the RatingsMacros module.
You can use shared examples.
These are typically saved under spec/support and loaded via spec_helper.rb. Be sure to read the docs to understand how to load the shared code--it is not automagically performed for you.
Once they are defined you can include them like so:
# spec/support/decorated_model.rb
shared_examples "decorated_model" do
it "can be decorated" do
subject.should respond_to?(:decorate)
end
end
# my_class_spec.rb
describe MyClass do
it_behaves_like "decorated_model"
end
module Share
def share
end
end
describe "Share Test" do
extend Share
end
You can call the methods of the module directly within the tests
The other answers pollute the test with the module's methods, or involve writing a dummy class. This solution uses the built-in double object as a throwaway object to extend with the module's methods.
RSpec.describe Share do
describe '#share' do
subject { double.extend(described_class) }
end
it 'does something cool' do
expect(subject.share).to eq 'something_cool'
end
end

Resources