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.
Related
I'm trying to see if it's possible to inject or execute gem's before filters in the parent app model.
Example:
Gem's gem/app/controllers/test/application_controller.rb:
module Test
class ApplicationController < ActionController::Base
before_update :foo
def foo
puts 'test'
end
end
end
Parent's App business.rb:
class Business < ApplicationRecord
# should call foo before creating the record
# and execute the puts statement
end
When the application executes Business.create(params), the business model should be calling method foo from the gem and executes the puts statement. Currently, I have no way of making this call, b/c foo is not executing before the create method. So I am looking for a way to try and test this. Thanks
Edit 2:
The use case here is each some of the models in the parent apps contain columns created_by and updated_by. We want to update these columns to the user UID that's doing the action on the specific model. I know I can do before_update or before_create in the parents app, but we want to put these actions in the gem so multiple of our applications can use it. The gem we are using is a common gem we wrote that integrated in all our applications. Also, I know there are gems out there like Audit-trail, but we want it simpler.
https://stackoverflow.com/a/35605007/14524531
Pretty much is the same idea with this snippet here, except using it in a gem with multiple models. I hope this makes it more clear. Thanks!
I have decided to test it with a new gem and a clean rails app.
Gem
Gemfile
source 'https://rubygems.org'
gemspec
gem 'activerecord'
lib/application_record.rb
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
before_save :foo
def foo
puts 'test'
end
end
Rails app
app/models/business.rb
class Business < ApplicationRecord
end
In rails console I call Business.create and the first output I get is test.
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
This is the first time I am writing test cases on a rails project which is using RSpec and FactoryGirl
When I run the test case i get the following error
wrong number of arguments (given 0, expected 1)
I have gone through other posts at stack over flow and they are not much helpful in my case.
What I have tried
I am writing a test case on a Model which is called ImportFeed and it looks something like as following
class ImportFeed < ApplicationRecord
belongs_to :staffroom
belongs_to :user, optional: true # We don't have to have a user
validates_presence_of :url, :feed_type
validates :enabled, presence: true, allow_blank: true
def initialize(params)
super(params)
self.enabled = false if self.enabled.blank?
self.default_radius = DEFAULT_RADIUS if self.default_radius.blank?
self.default_days = DAYS_DEFAULT if self.default_days.blank?
end
end
This is what my test case looks like
require 'rails_helper'
describe JobImporters::JoraJobImporter, '.run' do
it 'should create an instance of ImportFeed' do
feed = ImportFeed::new FactoryGirl.create(:import_feed, :import1)
expect(feed).to be_a ImportFeed
end
end
This is the factory
FactoryGirl.define do
factory :import_feed do
trait :import1 do
enabled true
feed_type 'example'
staffroom_id 7526
url Faker::Internet::url
end
end
end
When I run this I get the error mentioned at the beginning of this question,
If I pass the data to the test case without FactoryGirl then my test case works and passes for example if I replace
feed = ImportFeed::new FactoryGirl.create(:import_feed, :import1)
with
feed = ImportFeed::new enabled: true, staffroom_id: 7526, feed_type: 'example', url: Faker::Internet::url
the test case passes.
I will really appreciate if someone can point to me what am I doing wrong here.
Because you're overriding initialize method, so you got unexpected exception.
Don't override initialize on ActiveRecord objects
ActiveRecord::Base doesn't always use new to create objects, so initialize might not be called. [link]
In order to solve your problem, you should set your attributes in callback instead
class ImportFeed < ApplicationRecord
# ...
after_initialize :set_my_attributes
private
def set_my_attributes
self.enabled = false if self.enabled.blank?
self.default_radius = DEFAULT_RADIUS if self.default_radius.blank?
self.default_days = DAYS_DEFAULT if self.default_days.blank?
end
end
One more thing:
You're testing creating an instance of ImportFeed functionality, so you should either pass params to new or create methods to test it, but you pass an instance of ImportFeed to it (from FactoryGirl).
According to the docs, ActiveRecord#new accepts Hash only (the default argument is {} if you don't pass anything).
If you pass an object to it, you'll get ArgumentError exception along with "When assigning attributes, you must pass a hash as an argument" message
def assign_attributes(new_attributes)
if !new_attributes.respond_to?(:stringify_keys)
raise ArgumentError, "When assigning attributes, you must pass a hash as an argument."
end
return if new_attributes.empty?
attributes = new_attributes.stringify_keys
_assign_attributes(sanitize_for_mass_assignment(attributes))
end
I think you are just using the initialize method to change the values according to the conditions:
def initialize(params)
super(params)
self.enabled = false if self.enabled.blank?
self.default_radius = DEFAULT_RADIUS if self.default_radius.blank?
self.default_days = DAYS_DEFAULT if self.default_days.blank?
end
You should not override it (my suggestion) as it may break many things. So instead you may change the values on a callback (before_validation, before_save, before_create, after_initialize whichever suits you) like this:
before_create :set_default_radius, if: proc { |feed| feed.default_radius.blank? }
def set_default_radius
self.default_radius = DEFAULT_RADIUS
end
And the best way to do this is having the default value in database itself. You can define that in migration:
def up
change_column :import_feeds, :default_radius, :integer, default: 0
end
def down
change_column :import_feeds, :default_radius, :integer, default: nil
end
So if the value is not defined it will always set to the default value mentioned in the migration file.
Also you may have a read of several question related to this which has some very good answers and explanation:
How to override "new" method for a rails model
Why is overriding ActiveRecord::Base.initialize wrong?
Overriding ApplicationRecord initialize, bad idea?
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.
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.