In my model, I dynamically create some methods based on database records:
class Job < ActiveRecord::Base
belongs_to :job_status
# Adds #requisition?, #open?, #paused?, #closed?
class_eval do
JobStatus.all.each do |status|
unless method_defined? "#{status.name.downcase}?"
define_method("#{status.name.downcase}?") do
job_status_id == status.id
end
end
end
end
end
class JobStatus < ActiveRecord::Base
has_many :jobs
end
The job_statuses table contains some seed data, so is not going to be frequently changing, but in case I ever need to add new statuses, I don't have to add more code to get a boolean method for the new status.
However, I am not sure how to test these methods, because when rspec starts the job_statuses table is obviously empty, and when the JobStatus objects are created, Job gets initialized, but since no objects exist yet, it doesn't create any methods, and my tests fail because the methods don't exist.
Note that I am using rspec with spork & guard, and using database-cleaner with the truncation strategy (as per Railscast #257, since I'm using Selenium), so that probably complicates matters.
The solution I came up with was to abstract the creation of runtime methods out into a library file, and then in my test file, remove and redeclare my class before each test, and reload the actual class (and blueprints) at the end of the suite:
describe AssociationPredicate do
before(:all) do
["Continuous", "Standard"].each { |type| JobType.create!(:job_type => type) }
["Requisition", "Open", "Paused", "Closed"].each { |status| JobStatus.create!(:job_status => status) }
end
after(:all) do
DatabaseCleaner.clean_with :truncation, :only => %w( job_types job_statuses )
# Reload Job model to remove changes
Object.send(:remove_const, 'Job')
load 'job.rb'
load 'support/blueprints.rb'
end
before(:each) do
Object.send(:remove_const, 'Job')
# Redefine Job model for testing purposes
class Job < ActiveRecord::Base
belongs_to :job_type
belongs_to :job_status
has_many :job_applications
end
end
it "should add methods when included" do
Job.send(:association_predicate, :job_type)
job.should respond_to(:continuous?)
job.should respond_to(:standard?)
end
end
This way, I create a basic class for each test, add the runtime methods as necessarily, and return to the actual class when I'm done.
Try with enumerize gem. This make your status field like enumerator and build the "#{status.name.downcase}?" for your models. This gem came with it's own rspec-matchers making easiest your unit test.
Related
Background: I'm bringing to life a 6-year-old Rails project and haven't touched the framework since then. Thus, I'm re-learning many things.
I'm trying to understand the best approach to mock an API call that needs to be done synchronously. An Order has_one Invoice, and Invoice must get a reference from an external service. An Order is useless without an Invoice.
Below is a simple version of the application. The Order model is core to the application.
Open questions:
Is the best practise to globally mock SDKs in spec_helper.rb? Which would contain my allow_any_instance_of(InvoiceServiceSdk)
I have an Order factory, used almost everywhere in my tests. But I'm confused if I can loop in an Invoice factory as well. FactoryBot feels quite alien to me at the moment.
# app/models/order.rb
class Order < ApplicationRecord
has_one :invoice, autosave: true
before_create :build_invoice
def build_invoice
self.invoice = Invoice.new
end
end
# app/models/invoice.rb
class Invoice < ApplicationRecord
belongs_to :order
before_create :generate
def generate
invoice_service = InvoiceServiceSdk.new
self.external_id = invoice_service.fetch
end
end
# app/models/invoice_service_sdk.rb
require 'uri'
require 'net/http'
class InvoiceServiceSdk
def fetch
uri = URI('https://example.com/') # Real HTTP request
res = Net::HTTP.get_response(uri)
SecureRandom.urlsafe_base64 # "ID" that API "provides"
end
end
# spec/models/order.rb
require 'rails_helper'
RSpec.describe Order, type: :model do
before do
allow_any_instance_of(InvoiceServiceSdk).to receive(:fetch).and_return('super random external invoice ID')
end
context "new order + invoice" do
it {
o = Order.new
o.save
expect(o.invoice.external_id).to eq 'super random external invoice ID'
}
end
end
The rspec-mocks documentation discourages the use of allow_any_instance:
The rspec-mocks API is designed for individual object instances, but this feature operates on entire classes of objects. As a result there are some semantically confusing edge cases. For example, in expect_any_instance_of(Widget).to receive(:name).twice it isn't clear whether a specific instance is expected to receive name twice, or if two receives total are expected. (It's the former.)
Using this feature is often a design smell. It may be that your test is trying to do too much or that the object under test is too complex.
You can avoid it completely by just adding a factory method to your service objects:
class MyService
def intialize(**kwargs)
#options = kwargs
end
def call
do_something_awesome(#options[:foo])
end
def self.call(**kwargs)
new(**kwargs).call
end
end
allow(MyService).to recieve(:call).and_return([:foo, :bar, :baz])
Is it smelly to Stub the request in spec_helper?
Not necissarily. You can avoid a bit of overhead by refactoring the code as indicated above and stubbing the factory method. It also makes it so that your stubs are not coupled to the inner workings of the service object.
I would be more worried about the fact that this code does one thing right by using a service object and then immediately cancels that out by calling it in a model callback.
Reasonably green to testing but I was following along with a simple udemy course. I used the RSpec documentation to set up RSpec in rails to try out some testing. But I have come across an issue that for the life of me I can't figure out...
require "rails_helper"
RSpec.describe User, type: :model do
subject { described_class.new("John") }
it "initializes a name" do
expect(subject.name).to eq("John")
end
context "with no argument" do
subject { described_class.new }
it "should default to Bill as the name" do
expect(subject.name).to eq("Bill")
end
end
end
# This is my test code.
# This is my User model.
class User < ApplicationRecord
attr_reader :name
def initialize(name = "Bill")
#name = name
end
end
When I run the test it fails and says that the second test isn't returning Bill but 'nil'. However, in my User model if I remove the < Application Record it passes... Also, if I add a second parameter in the initialize, it randomly passes the default test and fails the first one returning the default name... I'm completely confused as I have been learning the testing without ApplicationRecord and that seems to be the part where it is failing. I have tried changing the subject to let(:testing){User.new} but that doesn't work. Any help seriously appreciated here as I can't seem to find it through google.
To let you know I have gem 'rspec-rails', '~> 4.0.0' included in my GemFile in the :development, :test part.
You are trying to override default initializer of a model and you doing it the wrong way. When you call new on ActiveRecord class you need to pass a hash of parameters. To have name field in a model you need to define it in DB schema.
Creation of an instance of User for the first test case should look like this:
described_class.new(name: "John")
I see these ways to have a default value for an attribute:
Set it using a callback
class User < ApplicationRecord
after_initialize :set_name
private
def set_name
self.name ||= 'Bill' # Set name to Bill if it is nil
end
end
Override initialize method.
# Please avoid this approach
class User < ApplicationRecord
def initialize(*args)
super # This will initiate default behaviour of a model
self.name ||= 'Bill'
end
end
Using attributes API as #engineersmnky suggested
class User < ApplicationRecord
attribute :name, :string, default: 'Bill'
end
I strongly recommend using a callback or attributes API approach to not broke default behaviour.
After that, your tests should pass I believe.
I can't figure how to test class method calls from within the class body.
How can I test it?
class User
act_as_paranoid
end
it 'is called from class body' do
expect(User).to receive(:acts_as_paranoid)
User.new
end
It's usually recommended to test the behavior, not the implementation. In this case, whatever acts_as_paranoid provides for this class in terms of behavior, is what you want to test.
However, if you trust that calling acts_as_paranoid correctly provides all the behavior you need and just want to test that it is added to the class, you can use:
assert User.included_modules.include? ActsAsParanoid::Core
To figure this out I just briefly looked at the source code for acts_as_paranoid here: https://github.com/ActsAsParanoid/acts_as_paranoid/blob/master/lib/acts_as_paranoid.rb#L8
You can see that on line 50, it extends the ActsAsParanoid module to ActiveRecord::Base, which gives the model classes access to the acts_as_paranoid method. And if you look at the definition of this method, you can see it calls include ActsAsParanoid::Core
Updated
This is not the greatest way to do this but if you must this is closer to what you want:
describe 'Check if a string method is in a file' do
it 'matches a string pattern' do
lines = File.read('user.rb').split("\n")
assert lines[1][/\b+acts_as_paranoid/]
#hacky way to make sure you don't accidentally comment it out
assert lines[1].split('#').count == 1
end
end
Original answer:
There is nothing here to test. Your class definition is invalid unless your method is defined when user.rb file loads. That is core ruby. Prove it.
#user_spec.rb
require 'minitest/autorun'
require_relative 'user'
describe 'User' do
it 'is a valid class' do
assert User
end
end
#user.rb
class User
acts_as_paranoid
end
If acts_as_paranoid is not defined before ruby loads user.rb, spec fails as soon as the file is required. If this is all the code you have this test fails. Comment out acts_as_paranoid test will pass.
To just test that you added acts_as_paranoid to User, you can do:
it 'has acts_as_paranoid' do
expect(User).to respond_to(:acts_as_paranoid)
end
MyClass.inspect return incorrect class when I run whole test suite.
Problem:
I have User::CreditCard and ActiveMerchant::Billing::CreditCard classes in project. Last from activemerchant gem.
When I run single spec(rspec spec/models/user/credit_card_spec.rb) then it works correctly.
When I run whole suite(rspec spec) then spec fails with undefined method..., it doesn't matter. The problem is that in this case, my CreditCard class is not mine!!!
When I run single spec and do puts User::CreditCard.inpsect(or just p User::CreditCard, or in pry just User::CreditCard) then it returns User::CreditCard as expected.
When I run whole suite and do p User::CreditCard inside spec then it returns ActiveMerchant::Billing::CreditCard.
Background:
If you don't want to read "background" then be sure that there are NOTE in the end
I'm working with legacy code. So I don't fully know all parts of the image.
I want to create Value Object for credit card in my User. So I've create new tableless model(note the path and class name):
#app/models/user/credit_card.rb
class User::CreditCard
include ActiveModel::Model
delegate :card_number, :card_expiration, :card_type, to: :subscription
def initialize(subscription)
#subscription = subscription || Subscription.new
end
private
attr_reader :subscription
end
Of course I have User model:
#app/models/user.rb
class User
...
has_one :subscription
...
def credit_card
#credit_card ||= User::CreditCard.new(subscription)
end
end
My specs for user/credit_card:
#spec/models/user/credit_card_spec.rb
require 'spec_helper'
# require 'user/credit_card' # if I include this then it works correct
RSpec.describe User::CreditCard, type: :model do
let(:subscription) { build :subscription }
let(:credit_card) do
p User::CreditCard # this result depends on whole/not whole suite run...
# rspec spec => ActiveMerchant::Billing::CreditCard
# rspec spec/models/user => User::CreditCard
User::CreditCard.new(subscription)
end
it 'should delegate alowed messages to user subscription' do
%w[card_number card_expiration card_type].each do |attr|
expect(credit_card.public_send(attr)).to eql subscription.public_send(attr)
end
end
it 'disallow another methods' do
expect { credit_card.unexisted_method }.to raise_error(NoMethodError)
end
end
NOTE:
in spec I can require 'user/credit_card' and then it will work. But why it does not work without it?
Can it be a problem in another places? For example in controllers or somewhere else?
This is a glitch of rails autoloading + ruby constant resolution.
class C; end
CONST = 42
C::CONST
#⇒ (pry):3: warning: toplevel constant CONST referenced by C::CONST
#⇒ 42
Surprisingly enough, CONST was resolved. That is because of Ruby constant resolution algorithm.
One has two options to fix the problem: either to give a different name to the class User::CreditCard or to make sure it’s loaded. Otherwise Rails finds the constant CreditCard in ActiveMerchant::Billing namespace and is happy with using it.
Suppose you have an ActiveRecord::Observer in one of your Ruby on Rails applications - how do you test this observer with rSpec?
You are on the right track, but I have run into a number of frustrating unexpected message errors when using rSpec, observers, and mock objects. When I am spec testing my model, I don't want to have to handle observer behavior in my message expectations.
In your example, there isn't a really good way to spec "set_status" on the model without knowledge of what the observer is going to do to it.
Therefore, I like to use the "No Peeping Toms" plugin. Given your code above and using the No Peeping Toms plugin, I would spec the model like this:
describe Person do
it "should set status correctly" do
#p = Person.new(:status => "foo")
#p.set_status("bar")
#p.save
#p.status.should eql("bar")
end
end
You can spec your model code without having to worry that there is an observer out there that is going to come in and clobber your value. You'd spec that separately in the person_observer_spec like this:
describe PersonObserver do
it "should clobber the status field" do
#p = mock_model(Person, :status => "foo")
#obs = PersonObserver.instance
#p.should_receive(:set_status).with("aha!")
#obs.after_save
end
end
If you REALLY REALLY want to test the coupled Model and Observer class, you can do it like this:
describe Person do
it "should register a status change with the person observer turned on" do
Person.with_observers(:person_observer) do
lambda { #p = Person.new; #p.save }.should change(#p, :status).to("aha!)
end
end
end
99% of the time, I'd rather spec test with the observers turned off. It's just easier that way.
Disclaimer: I've never actually done this on a production site, but it looks like a reasonable way would be to use mock objects, should_receive and friends, and invoke methods on the observer directly
Given the following model and observer:
class Person < ActiveRecord::Base
def set_status( new_status )
# do whatever
end
end
class PersonObserver < ActiveRecord::Observer
def after_save(person)
person.set_status("aha!")
end
end
I would write a spec like this (I ran it, and it passes)
describe PersonObserver do
before :each do
#person = stub_model(Person)
#observer = PersonObserver.instance
end
it "should invoke after_save on the observed object" do
#person.should_receive(:set_status).with("aha!")
#observer.after_save(#person)
end
end
no_peeping_toms is now a gem and can be found here: https://github.com/patmaddox/no-peeping-toms
If you want to test that the observer observes the correct model and receives the notification as expected, here is an example using RR.
your_model.rb:
class YourModel < ActiveRecord::Base
...
end
your_model_observer.rb:
class YourModelObserver < ActiveRecord::Observer
def after_create
...
end
def custom_notification
...
end
end
your_model_observer_spec.rb:
before do
#observer = YourModelObserver.instance
#model = YourModel.new
end
it "acts on the after_create notification"
mock(#observer).after_create(#model)
#model.save!
end
it "acts on the custom notification"
mock(#observer).custom_notification(#model)
#model.send(:notify, :custom_notification)
end