rspec testing delegation for module method undefined method - ruby-on-rails

This module gets included in a form object within rails.
What is the right way to test it using rspec?
1) Do I test it directly on each model that includes it?
or
2) Do I test the delegation method directly? (i would prefer direct if possible)
If I test it directly, how? I tried and get the below error...
Form Object Module
module Registration
class Base
module ActAsDelegation
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
def form_fields_mapping
[
{name: :first, model: :user},
{name: :address, model: :address}
]
end
def fields_of_model(model)
form_fields_mapping.select {|record| record[:model] == model }.map {|record| record[:name] }
end
def delegate_fields_to(*models)
models.each do |model|
fields_of_model(model).each do |attr|
delegate attr.to_sym, "#{attr}=".to_sym, to: model if attr.present?
end
end
end
end
end
end
end
Form Object
module Registration
class Base
include ActiveModel::Model
include ActAsDelegation
def initialize(user=nil, attributes={})
error_msg = "Can only initiate inherited Classes of Base, not Base Directly"
raise ArgumentError, error_msg if self.class == Registration::Base
#user = user
setup_custom_accessors
unless attributes.nil?
(self.class.model_fields & attributes.keys.map(&:to_sym)).each do |field|
public_send("#{field}=".to_sym, attributes[field])
end
end
validate!
end
end
end
RSPEC TESTING
require "rails_helper"
RSpec.describe Registration::Base::ActAsDelegation, type: :model do
describe "Class Methods" do
context "#delegate_fields_to" do
let(:user) {spy('user')}
let(:address) {spy('address')}
let(:delegation_fields) { [
{name: :first, model: :user},
{name: :address, model: :address}
]}
it "should delegate" do
allow(subject).to receive(:form_fields_mapping) { delegation_fields }
Registration::Base::ActAsDelegation.delegate_fields_to(:user,:address)
expect(user).to have_received(:first)
expect(address).to have_received(:address)
end
end
end
end
ERROR
Failure/Error:
Registration::Base::ActAsDelegation.delegate_fields_to(:user,:address)
NoMethodError:
undefined method `delegate_fields_to' for Registration::Base::ActAsDelegation:Module
Did you mean? delegate_missing_to
(I have other code issues in this example, but below resoved the main issue)

As your module is designed to be included, just include it in an empty class in tests. I prepared a simplified example which I verified to work:
module ToBeIncluded
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
def a_class_method
:class
end
end
end
class TestSubject
include ToBeIncluded
end
require 'rspec/core'
RSpec.describe ToBeIncluded do
subject { TestSubject }
it 'returns correct symbol' do
expect(subject.a_class_method).to eq(:class)
end
end
In your case probably something along those lines should be fine:
require "rails_helper"
RSpec.describe Registration::Base::ActAsDelegation, type: :model do
class TestClass
include Registration::Base::ActAsDelegation
end
describe "Class Methods" do
context "#delegate_fields_to" do
let(:user) {spy('user')}
let(:address) {spy('address')}
let(:delegation_fields) { [
{name: :first, model: :user},
{name: :address, model: :address}
]}
it "should delegate" do
allow(TestClass).to receive(:form_fields_mapping) { delegation_fields }
TestClass.delegate_fields_to(:user,:address)
expect(user).to have_received(:first)
expect(address).to have_received(:address)
end
end
end
end
Also, you could make anonymous class if you are afraid of name clashes.

Related

FactoryBot factory for a Poro results in undefined method `build'

I have a simple Poro, like so:
class Student
attr_reader :first_name, :last_name
def initialize(data)
#first_name = data[:first_name]
#last_name = data[:last_name]
end
end
A factory like so:
FactoryBot.define do
factory :student do
first_name {"test first name"}
last_name {"test last name"}
# https://thoughtbot.com/blog/tips-for-using-factory-girl-without-an-orm
initialize_with { new(attributes) }
end
end
A test like so:
describe 'StudentSpec', type: :model do
let(:student) {build(:student)}
context 'attributes' do
it 'respond' do
expect(student).to respond_to(:first_name, :last_name)
end
end
end
But this results in NoMethodError: undefined method 'build' for ....
Based on https://thoughtbot.com/blog/tips-for-using-factory-girl-without-an-orm, it sounds like this should work. Wondering what I am doing wrong?
Maybe you are missing require 'rails_helper' at the top of the spec file?
Also did you try to add FactoryBot?
let(:student) { FactoryBot.build(:student) }

How to test class method using rspec

Hi i am working on RoR project with ruby-2.5.0 and rails 5.0. I have a model forgot_password where a class method is defined to create a record as follows:-
forgot_password.rb
# frozen_string_literal: true
class ForgotPassword < ApplicationRecord
before_create :create_token
def self.create_record
self.create!(expiry: Time.zone.now +
ENV['VALIDITY_PERIOD'].to_i.hours)
end
private
def create_token
self.token = SecureRandom.urlsafe_base64(nil, false)
end
end
I want to write unit testing for it using stub or factory_girl gem.
spec/models/forgot_password_spec.rb
# frozen_string_literal: true
require 'rails_helper'
describe ForgotPassword do
let(:forgot_password) do
described_class.new()
end
describe 'create_record' do
context 'with forgot_password class' do
subject { forgot_password.create_record.class }
it { is_expected.to eq ForgotPassword }
end
end
end
But its throwing error undefined method create_record for #<ForgotPassword:0x000000000622bc98> Please help me how can i test my model. Thanks in advance.
What you have written is a factory method (a class method that returns an instance) you should call it and write expectations about the instance returned:
describe ForgotPassword do
describe ".create_record" do
subject { described_class.create_record! }
it { is_expected.to be_an_instance_of(described_class) }
it "sets the expiry time to a time in the future" do
expect(subject.expiry > Time.now).to be_truthy
end
end
end
However if what you really are looking to do is set a computed default value then there is a much less clunky way:
class ForgotPassword < ApplicationRecord
after_initialize :set_expiry!
private
def set_expiry!
self.expiry(expiry: Time.zone.now).advance(hours: ENV['VALIDITY_PERIOD'].to_i)
end
end
Or with Rails 5:
class ForgotPassword < ApplicationRecord
attribute :expiry, :datetime,
->{ Time.zone.now.advance(hours: ENV['VALIDITY_PERIOD'].to_i) }
end
You can test it by:
describe ForgotPassword do
let(:forgot_password){ described_class.new }
it "has a default expiry" do
expect(forgot_password.expiry > Time.now).to be_truthy
end
end
You can test against described_class directly:
require 'rails_helper'
describe ForgotPassword do
context 'with forgot_password class' do
subject { described_class }
it { is_expected.to eq ForgotPassword }
end
end

RSpec how to mock controller method inside concern?

I have the following:
class PaymentController < ActionController::API
include ObjectActions
def error_notification(message)
puts "An error has occurred: #{message}"
end
end
module ObjectActions
extend ActiveSupport::Concern
def process
if valid?
# process payment
else
error_notification("Payment is not valid")
end
end
end
Now, I'm trying to mock/stub the "external" error_notification method inside ObjectActions module/concern.
RSpec.describe ObjectActions, type: :concern do
include ObjectActions
before do
allow(described_class).to receive(:valid?).and_return(false)
# I KNOW THIS IS NOT RIGHT, HOW CAN I PROPERLY MOCK IT?
allow(described_class).to receive(:error_notification).and_return("Blah blah")
end
context '#process' do
it { expect { process }.to eq("Blah blah") }
end
end
Short answer would be
allow(self).to receive(:error_notification).and_return("Blah blah")
Why?
You're including the module, you want to test in the current test
RSpec.describe ObjectActions, type: :concern do
include ObjectActions
So this is what you're supposed to mock. But it's a bit better way to do it, which I described not long ago in this answer: https://stackoverflow.com/a/48914463/299774

ArgumentError: wrong number of arguments (0 for 2) in initialize with inheritance

Okay,
this is driving me nuts, since I don't understand the error in this case.
I have the following class defined:
module Admins
class BasePresenter < ::BasePresenter
def render_customer(id:)
return I18n.t('admin.admin') if id.nil?
::Customer.where(id: id).first.try(:name) || I18n.t('admin.deleted')
end
def percent_of(count, total)
((count.to_f / total.to_f) * 100.0).to_i
end
end
end
Which inherits from the BasePresenter below:
class BasePresenter
def initialize(object, template)
#object = object
#template = template
end
def self.presents(name)
define_method(name) do
#object
end
end
def underscored_class
#object.class.name.underscore
end
protected
def h
#template
end
def handle_none(value, html = true)
if value.present?
if block_given?
yield
else
value
end
else
return h.content_tag(:span, '-', class: 'none') if html
'-'
end
end
def current_customer
#current_customer ||= h.current_customer
end
def current_user
#current_user ||= h.current_user
end
end
However when I try to run my specs, I receive the following error from RSpec:
ArgumentError: wrong number of arguments (0 for 2)
./app/presenters/base_presenter.rb:3:in initialize'
./spec/presenters/admins/base_presenter_spec.rb:24:inblock (3
levels) in '
The class is no different from other presents, where the the inheritance works in the exact same way and those tests are passing.
Just the test for this class is failing with this error, and only when testing the method percent_of.
What am I failing to see?
EDIT
This is my RSpec test:
require 'spec_helper'
describe ::Admins::BasePresenter do
describe '#render_customer' do
let(:customer) { Customer.first }
subject { ::Admins::BasePresenter.new(Object.new, ApplicationController.new.view_context) }
it 'returns the I18n translations for (admin) when no customer is set.' do
expect(subject.render_customer(id: nil)).to eql(I18n.t('admin.admin'))
end
it 'returns the proper name when a valid ID is given' do
expect(subject.render_customer(id: customer.id)).to eql(customer.name)
end
it 'returns the I18n translations for (deleted) when an invalid ID is given' do
expect(subject.render_customer(id: -1)).to eql(I18n.t('admin.deleted'))
end
end
describe '#percent_of' do
it 'calculates the percentage correctly' do
expect(subject.percent_of(0, 1)).to eql(0)
expect(subject.percent_of(1, 1)).to eql(100)
expect(subject.percent_of(1, 2)).to eql(50)
expect(subject.percent_of(1, 3)).to eql(33)
end
end
end
Ugh,
I'm an idiot....
The problem was that my subject was defined inside a Describe block for specific tests and the second one did not have any.
Which means our hooks try to create an instance of the class in the outer describe block...
This was the fix:
require 'spec_helper'
describe ::Admins::BasePresenter do
let(:customer) { Customer.first }
subject { ::Admins::BasePresenter.new(Object.new, ApplicationController.new.view_context) }
describe '#render_customer' do
it 'returns the I18n translations for (admin) when no customer is set.' do
expect(subject.render_customer(id: nil)).to eql(I18n.t('admin.admin'))
end
it 'returns the proper name when a valid ID is given' do
expect(subject.render_customer(id: customer.id)).to eql(customer.name)
end
it 'returns the I18n translations for (deleted) when an invalid ID is given' do
expect(subject.render_customer(id: -1)).to eql(I18n.t('admin.deleted'))
end
end
describe '#percent_of' do
it 'calculates the percentage correctly' do
expect(subject.percent_of(0, 1)).to eql(0)
expect(subject.percent_of(1, 1)).to eql(100)
expect(subject.percent_of(1, 2)).to eql(50)
expect(subject.percent_of(1, 3)).to eql(33)
end
end
end

Rails & RSpec - Testing Concerns class methods

I have the following (simplified) Rails Concern:
module HasTerms
extend ActiveSupport::Concern
module ClassMethods
def optional_agreement
# Attributes
#----------------------------------------------------------------------------
attr_accessible :agrees_to_terms
end
def required_agreement
# Attributes
#----------------------------------------------------------------------------
attr_accessible :agrees_to_terms
# Validations
#----------------------------------------------------------------------------
validates :agrees_to_terms, :acceptance => true, :allow_nil => :false, :on => :create
end
end
end
I can't figure out a good way to test this module in RSpec however - if I just create a dummy class, I get active record errors when I try to check that the validations are working. Has anyone else faced this problem?
Check out RSpec shared examples.
This way you can write the following:
# spec/support/has_terms_tests.rb
shared_examples "has terms" do
# Your tests here
end
# spec/wherever/has_terms_spec.rb
module TestTemps
class HasTermsDouble
include ActiveModel::Validations
include HasTerms
end
end
describe HasTerms do
context "when included in a class" do
subject(:with_terms) { TestTemps::HasTermsDouble.new }
it_behaves_like "has terms"
end
end
# spec/model/contract_spec.rb
describe Contract do
it_behaves_like "has terms"
end
You could just test the module implicitly by leaving your tests in the classes that include this module. Alternatively, you can include other requisite modules in your dummy class. For instance, the validates methods in AR models are provided by ActiveModel::Validations. So, for your tests:
class DummyClass
include ActiveModel::Validations
include HasTerms
end
There may be other modules you need to bring in based on dependencies you implicitly rely on in your HasTerms module.
I was struggling with this myself and conjured up the following solution, which is much like rossta's idea but uses an anonymous class instead:
it 'validates terms' do
dummy_class = Class.new do
include ActiveModel::Validations
include HasTerms
attr_accessor :agrees_to_terms
def self.model_name
ActiveModel::Name.new(self, nil, "dummy")
end
end
dummy = dummy_class.new
dummy.should_not be_valid
end
Here is another example (using Factorygirl's "create" method" and shared_examples_for)
concern spec
#spec/support/concerns/commentable_spec
require 'spec_helper'
shared_examples_for 'commentable' do
let (:model) { create ( described_class.to_s.underscore ) }
let (:user) { create (:user) }
it 'has comments' do
expect { model.comments }.to_not raise_error
end
it 'comment method returns Comment object as association' do
model.comment(user, "description")
expect(model.comments.length).to eq(1)
end
it 'user can make multiple comments' do
model.comment(user, "description")
model.comment(user, "description")
expect(model.comments.length).to eq(2)
end
end
commentable concern
module Commentable
extend ActiveSupport::Concern
included do
has_many :comments, as: :commentable
end
def comment(user, description)
Comment.create(commentable_id: self.id,
commentable_type: self.class.name,
user_id: user.id,
description: description
)
end
end
and restraunt_spec may look something like this (I'm not Rspec guru so don't think that my way of writing specs is good - the most important thing is at the beginning):
require 'rails_helper'
RSpec.describe Restraunt, type: :model do
it_behaves_like 'commentable'
describe 'with valid data' do
let (:restraunt) { create(:restraunt) }
it 'has valid factory' do
expect(restraunt).to be_valid
end
it 'has many comments' do
expect { restraunt.comments }.to_not raise_error
end
end
describe 'with invalid data' do
it 'is invalid without a name' do
restraunt = build(:restraunt, name: nil)
restraunt.save
expect(restraunt.errors[:name].length).to eq(1)
end
it 'is invalid without description' do
restraunt = build(:restraunt, description: nil)
restraunt.save
expect(restraunt.errors[:description].length).to eq(1)
end
it 'is invalid without location' do
restraunt = build(:restraunt, location: nil)
restraunt.save
expect(restraunt.errors[:location].length).to eq(1)
end
it 'does not allow duplicated name' do
restraunt = create(:restraunt, name: 'test_name')
restraunt2 = build(:restraunt, name: 'test_name')
restraunt2.save
expect(restraunt2.errors[:name].length).to eq(1)
end
end
end
Building on Aaron K's excellent answer here, there are some nice tricks you can use with described_class that RSpec provides to make your methods ubiquitous and make factories work for you. Here's a snippet of a shared example I recently made for an application:
shared_examples 'token authenticatable' do
describe '.find_by_authentication_token' do
context 'valid token' do
it 'finds correct user' do
class_symbol = described_class.name.underscore
item = create(class_symbol, :authentication_token)
create(class_symbol, :authentication_token)
item_found = described_class.find_by_authentication_token(
item.authentication_token
)
expect(item_found).to eq item
end
end
context 'nil token' do
it 'returns nil' do
class_symbol = described_class.name.underscore
create(class_symbol)
item_found = described_class.find_by_authentication_token(nil)
expect(item_found).to be_nil
end
end
end
end

Resources