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.
Related
I don't have a great experience with mixin modules. Then, please forgive me if my question seems to be a bit naïve.
I am creating a few modules to integrate a project with music services like Spotify, who have REST APIs. All these modules include another mixin module I created named APIClientBuilder, which provides a small DSL for creating API endpoints.
lib/integrations/api_client_builder.rb
require 'rest-client'
module APIClientBuilder
attr_accessor :api_client, :endpoint, :url, :param
def api_client(api_name)
end
def fetch_client(api_name)
end
def api_endpoint(endpoint_name)
end
def fetch_endpoint(api_name,endpoint_name)
end
def method=(meth)
end
def url=(endpoint_url)
end
def param(param_name,param_value)
end
def call(api_name,api_endpoint,token,*extra_params)
end
end
lib/integrations/spotify.rb
require_relative 'api_client_builder'
module SpotifyIntegration
include APIClientBuilder
def base_url
'https://api.spotify.com/v1'
end
def random_state_string
(0..10).map { (65 + rand(26)).chr }.join
end
api_client('spotify') do |apic|
apic.api_endpoint('request_authorization') do |ep|
ep.method = :get
ep.url = "https://accounts.spotify.com/authorize"
ep.param("client_id",SPOTIFY_KEY)
ep.param("response_type","code")
ep.param("redirect_uri","http://localhost:3000")
end
apic.api_endpoint('my_playlists') do |ep|
ep.method = :get
ep.url = "#{base_url}/me/playlists"
end
end
end
My idea was having in my controllers something like this:
app/controllers/api/v1/users_controller.rb
require 'integrations/spotify.rb'
class UsersController < ApplicationController
include SpotifyIntegration
end
And then have access to the methods in SpotifyIntegration and, through this, to the methods in APIClientBuilder.
It happens that I wrote the following spec file with a very simple test:
spec/lib/integrations/spotify_integration_spec.rb
require 'rails_helper'
require 'integrations/spotify'
class SpotifyClientTester
include SpotifyIntegration
end
RSpec.describe SpotifyIntegration do
context 'Auxiliary methods' do
it 'Two calls to random_state_string shall generate two different strings' do
obj = SpotifyClientTester.new
s1 = obj.random_state_string
s2 = obj.random_state_string
expect(s1).not_to eq(s2)
end
end
end
But when I run it I get
undefined local variable or method base_url for SpotifyIntegration:Module (NameError)
I am not sure about what I am missing. Maybe I should use extend instead of include. I always make some confusion about this.
Can someone put me in the right path? I've been fighting this error for a whole afternoon.
You're misusing mixins. Use mixins for cases where classical inheritance is not suited to add a set of features to objects.
For example:
module Commentable
extend ActiveSupport::Concern
included do
has_many :comments, as: :commentable
end
# ...
end
class Video < ApplicationRecord
include Commentable
end
class Hotel < ApplicationRecord
include Commentable
end
As you can see by this example you extend a module with other modules and include modules in classes. Using classical inheritance to add the shared behaviour would be awkward at best since the two classes are apples and pears.
In your specific case you should instead use classical inheritance and not mix the API client into the controller. Rather you controller should invoke it as a distinct object.
class APIClient
# Implement shared behavior for a REST api client
end
class SpotifyClient < APIClient
# ...
end
class FoosController < ApplicationController
def index
client = SpotifyClient.new
#foos = client.get_something
end
end
Why shouldn't you mix a API client into a controller or model? Because of the Single Responsibility Principle and the fact that using smaller parts that do a limited amount of things is preferable to creating god classes.
You need to extend APIClientBuilder if you want to use the methods defined here at class level in module SpotifyIntegration.
module SpotifyIntegration
extend APIClientBuilder
Also, base_url must be a class method too, def self.base_url
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.
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
I've got a module in my project in lib/. it's content is like this :
module Search
module Score
def get_score
return 'something'
end
end
end
This Search has many different modules I need to use Score. I realize I need to add require in my model (I'm trying to use this from model). So here is my code (model) :
require 'search'
class User < ActiveRecord::Base
def get_user_score
#tried this :
p Search::Score.get_score #error
#this as well
score_instance = Score.new #error
score = Search::Score.get_score # error undefined method `get_score'
end
end
So how do I reuse the code I have in other class (module)?
To get it working you can either mix the module into your class:
require 'search'
class User < ActiveRecord::Base
include Search::Score
def get_user_score
p get_score # => "something"
end
end
Or you can define the method inside your module similar to class methods:
module Search
module Score
def self.get_score
return 'something'
end
end
end
If you do that, you can call get_score like expected:
require 'search'
class User < ActiveRecord::Base
def get_user_score
p Search::Score.get_score # => "something"
end
end
See this tutorial for a more in depth explanation about modules in Ruby.
First, see "Best Practices for reusing code between controllers in Ruby on Rails".
About reuse code as a module, take a look at "Rethinking code reuse with Modularity for Ruby".
"Modules are crippled classes"
Modules are like crippled classes in Ruby. If you look into the inheritance chain you see that a Class actually inherits from Module.
Module cannot be instanciated. So the call to .new is not working.
What you CAN do however is to specify your method as a 'class' method (I know I said it is not a class...)
So you would add a self in front like this:
module Search
module Score
def self.get_score
return 'something'
end
end
end
Then you can call this method as a class method like you tried in your code example
Search::Score is a module and not a class, so Score.new will not work.
You can try to change the signature of the get_score function to self.get_score.
In addition to def self.get_score in the above answers, there is also extend self, like so:
module Search
module Score
extend self
def get_score
return 'something'
end
end
end
and module_function:
module Search
module Score
module_function
def get_score
return 'something'
end
end
end
The latter is actually the preferred method in RuboCop (source), though in practice I personally have not seen it so often.
Can we use include statement to include a module anywhere within the class or does it has to be at the beginning of the class?
If I include the module at the beginning of my class declaration, method overriding works as expected. Why is it not working if i include at the end as described below?
# mym.rb
module Mym
def hello
puts "am in the module"
end
end
# myc.rb
class Myc
require 'mym'
def hello
puts "am in class"
end
include Mym
end
Myc.new.hello
=> am in class
When you include a module, its methods do NOT replace methods defined in this class, but rather they are injected into inheritance chain. So, when you call super, method from included module will get called.
They will behave almost the same way with other modules. When a module gets included, it is placed right above the class in inheritance chain, with existing modules placed above it. See example:
module Mym
def hello
puts "am in the module"
end
end
module Mym2
def hello
puts "am in the module2"
super
end
end
class Myc
include Mym
include Mym2
def hello
puts "im in a class"
super
end
end
puts Myc.new.hello
# im in a class
# am in the module2
# am in the module
For more info see this post.
Also read this: http://rhg.rubyforge.org/chapter04.html