shoulda-matchers fail when attribute saved in a model's callback - ruby-on-rails

class StudentPiggyBank < ActiveRecord::Base
PERIODS = [['tydzień', :week], ['miesiąc', :month], ['trzy miesiące', :three_months]]
RATES_MULTIPLIERS = {week: 1, month: 1.5, three_months: 2}
INTEREST_RATE_PRECISION = 2
before_validation :set_interest_rate
validates :completion_date, presence: true
validates :balance, numericality: {greater_than_or_equal_to: 0,
message: I18n.t('errors.messages.negative_piggy_bank_balance')}
validates :interest_rate, numericality: {greater_than_or_equal_to: 0,
message: I18n.t('errors.messages.negative_interest_rate')}
def self.date_from_param(period_param)
case period_param
when 'week'
1.week.from_now
when 'month'
1.month.from_now
when 'three_months'
3.months.from_now
end
end
protected
def set_interest_rate
num_of_days = completion_date - Date.today
if num_of_days >= 90
self.interest_rate = student.base_interest_rate.mult(RATES_MULTIPLIERS[:three_months], INTEREST_RATE_PRECISION)
elsif num_of_days >= 30
self.interest_rate = student.base_interest_rate.mult(RATES_MULTIPLIERS[:month], INTEREST_RATE_PRECISION)
else
self.interest_rate = student.base_interest_rate.mult(RATES_MULTIPLIERS[:week], INTEREST_RATE_PRECISION)
end
end
end
This code works. However, when testing with shoulda-matchers
describe StudentPiggyBank do
it { should validate_numericality_of(:interest_rate).is_greater_than_or_equal_to(0) }
it { should validate_numericality_of(:balance).is_greater_than_or_equal_to(0) }
end
I get errors for the line num_of_days = completion_date - Date.today:
NoMethodError:
undefined method `-' for nil:NilClass
Why completion_date is nil?

Well, it will basically do a described_class.new, so you won't have a completion_date. You can fix it like this:
describe StudentPiggyBank do
context 'with a completion date' do
before { subject.completion_date = 7.days.from_now }
it { should validate_numericality_of(:interest_rate).is_greater_than_or_equal_to(0) }
end
end

Related

Implementation Value Object pattern for ActiveRecord attribute

The task was allow user to view, create, edit records in different units, i.e. altitude in meters and feets, speed in m/s, knots, km/h, mi/h.
I've read a lot about Value Objects, composed_of and why we should not use it and use serialize instead and came with solution below.
It seems like complex solution for me.
Could you please point me what can I refactor and direction for it?
app/models/weather.rb
class Weather < ActiveRecord::Base
attr_accessor :altitude_unit, :wind_speed_unit
attr_reader :altitude_in_units, :wind_speed_in_units
belongs_to :weatherable, polymorphic: true
before_save :set_altitude, :set_wind_speed
validates_presence_of :actual_on, :wind_direction
validate :altitude_present?
validate :wind_speed_present?
validates_numericality_of :altitude, greater_than_or_equal_to: 0, allow_nil: true
validates_numericality_of :altitude_in_units, greater_than_or_equal_to: 0, allow_nil: true
validates_numericality_of :wind_speed, greater_than_or_equal_to: 0, allow_nil: true
validates_numericality_of :wind_speed_in_units, greater_than_or_equal_to: 0, allow_nil: true
validates_numericality_of :wind_direction, greater_than_or_equal_to: 0, less_than: 360, allow_nil: true
serialize :altitude, Distance
serialize :wind_speed, Velocity
def wind_speed_in_units=(value)
#wind_speed_in_units = value_from_param(value)
end
def altitude_in_units=(value)
#altitude_in_units = value_from_param(value)
end
private
def value_from_param(value)
return nil if value.is_a?(String) && value.empty?
value
end
def altitude_present?
return if altitude.present? || altitude_in_units.present?
errors.add :altitude, :blank
end
def wind_speed_present?
return if wind_speed.present? || wind_speed_in_units.present?
errors.add :wind_speed, :blank
end
def set_altitude
return if altitude_in_units.blank? || altitude_unit.blank?
self.altitude = Distance.new(altitude_in_units, altitude_unit)
end
def set_wind_speed
return if wind_speed_in_units.blank? || wind_speed_unit.blank?
self.wind_speed = Velocity.new(wind_speed_in_units, wind_speed_unit)
end
end
app/model/distance.rb
class Distance < DelegateClass(BigDecimal)
FT_IN_M = 3.280839895
def self.load(distance)
new(distance) unless distance.nil?
end
def self.dump(obj)
obj.dump
end
def initialize(distance, unit = 'm')
value = convert_from(BigDecimal.new(distance), unit)
super(value)
end
def dump
#delegate_dc_obj
end
def convert_to(unit)
method = "to_#{unit}"
raise ArgumentError, "Unsupported unit #{unit}" unless respond_to? method
send method
end
def convert_from(val, unit)
method = "from_#{unit}"
raise ArgumentError, "Unsupported unit #{unit}" unless respond_to? method
send method, val
end
def to_m
#delegate_dc_obj
end
def to_ft
#delegate_dc_obj * FT_IN_M
end
def from_m(val)
val
end
def from_ft(val)
val / FT_IN_M
end
end
app/models/velocity.rb is almost the same as distance.

rails 4 refactor factorygirl creates inaccurate data

I have been battling a major refactor to slim down a payments controller and could use a hand. Step one I am trying to fix my factories. Right now all of the factories work great on their own, but when I try to build associations the FactoryGirl.create(:job, :purchased_with_coupon) it will setup the association correctly on the coupon but not the payment. This means that the price paid is always is always 1. I just noticed this which you can see the other section commented out. Before I start tackling the bloated controller I need to figure this out for my tests. Thoughts?
Factories
FactoryGirl.define do
factory :job do
category
company
title { FFaker::Company.position }
location { "#{FFaker::Address.city}, #{FFaker::AddressUS.state}" }
language_list { [FFaker::Lorem.word] }
short_description { FFaker::Lorem.sentence }
description { FFaker::HTMLIpsum.body }
application_process { "Please email #{FFaker::Internet.email} about the position." }
trait :featured do |job|
job.is_featured true
end
trait :reviewed do |job|
job.reviewed_at { Time.now }
end
trait :purchased do |job|
job.reviewed_at { Time.now }
job.start_at { Time.now }
job.end_at { AppConfig.product['settings']['job_active_for_day_num'].day.from_now }
job.paid_at { Time.now }
payments { |j| [j.association(:payment)] }
end
trait :purchased_with_coupon do |job|
job.reviewed_at { Time.now }
job.start_at { Time.now }
job.end_at { AppConfig.product['settings']['job_active_for_day_num'].day.from_now }
job.paid_at { Time.now }
association :coupon, factory: :coupon
payments { |j| [j.association(:payment)] }
end
trait :expired do |job|
start_at = (200..500).to_a.sample.days.ago
job.reviewed_at { start_at }
job.start_at { start_at }
job.end_at { |j| j.start_at + AppConfig.product['settings']['job_active_for_day_num'].days }
job.paid_at { start_at }
payments { |j| [j.association(:payment)] }
end
end
end
FactoryGirl.define do
factory :payment do
job
# price_paid { rand(100..150) }
price_paid { 1 }
stripe_customer_token { (0...50).map { (65 + rand(26)).chr }.join }
end
end
FactoryGirl.define do
factory :coupon do
code { rand(25**10) }
percent_discount { rand(100**1) }
start_at { 2.days.ago }
end_at { 30.day.from_now }
trait :executed do |c|
association :job, factory: [:job, :purchased]
c.executed_at { Time.now }
end
end
end
Models
class Job < ActiveRecord::Base
acts_as_paranoid
strip_attributes
acts_as_taggable
acts_as_taggable_on :languages
belongs_to :company
before_validation :find_company
belongs_to :category
has_one :coupon
has_many :payments
before_create :create_slug, :set_price
after_create :update_vanity_url
accepts_attachments_for :company
accepts_nested_attributes_for :company
accepts_nested_attributes_for :coupon
accepts_nested_attributes_for :payments
validates :title,
:location,
:short_description,
presence: true,
format: { with: /\A[\w\d .,:-#]+\z/, message: :bad_format }
validates :application_process,
presence: true,
format: { with: %r{\A[\w\d .,:/#&=?-]+\z}, message: :bad_format }
validates :title, length: { minimum: 10, maximum: 45 }
validates :location, length: { minimum: 10, maximum: 95 }
validates :short_description, length: { minimum: 10, maximum: 245 }
validates :application_process, length: { minimum: 10, maximum: 95 }
validates :description,
:category_id,
:language_list,
presence: true
validates :reviewed_at,
:start_at,
:end_at,
:paid_at,
date: { allow_blank: true }
validates :start_at, date: { before: :end_at, message: :start_at_before_end_at }, if: proc { start_at? }
validates :end_at, date: { after: :start_at, message: :end_at_after_start_at }, if: proc { end_at? }
scope :active, -> { where.not(reviewed_at: nil, paid_at: nil).where('end_at >= ?', Date.today) }
def expired?
end_at.present? && end_at < Date.today
end
def reviewed?
reviewed_at.present?
end
def paid_for?
reviewed? && paid_at.present?
end
def active?
reviewed? && paid_at.present? && end_at <= Date.today
end
private
def set_price
self.price = AppConfig.product['settings']['job_base_price']
end
def create_slug
self.slug = title.downcase.parameterize
end
def update_vanity_url
self.vanity_url = '/jobs/' + company.slug + '/' + slug + '/' + id.to_s + '/'
save
end
def find_company
existing_company = Company.where(email: company.email) if company
self.company = existing_company.first if existing_company.count > 0
end
end
class Coupon < ActiveRecord::Base
acts_as_paranoid
strip_attributes
belongs_to :job
validates :start_at, date: { before: :end_at }
validates :executed_at, date: { allow_blank: true }
validates_presence_of :job, if: proc { executed_at? }
validates_presence_of :executed_at, if: :job
validates :code,
presence: true,
length: { minimum: 10, maximum: 19 },
uniqueness: { case_sensitive: false },
numericality: { only_integer: true }
validates :percent_discount,
inclusion: { in: 1..100 },
length: { minimum: 1, maximum: 3 },
numericality: { only_integer: true },
presence: true
scope :active, -> { where('start_at < ? AND end_at > ? AND executed_at IS ?', Date.today, Date.today, nil) }
def active?
start_at < Date.today && end_at > Date.today && executed_at.nil?
end
def executed?
job_id.present?
end
end
class Payment < ActiveRecord::Base
belongs_to :job
belongs_to :coupon
validates_presence_of :job
validate :coupon_must_be_active
before_create :net_price
Numeric.include CoreExtensions::Numeric::Percentage
attr_accessor :coupon_code
def coupon_code=(code)
#coupon = Coupon.find_by_code(code)
end
def net_price
return job.price unless #coupon
job.price = #coupon.percent_discount.percent_of(job.price)
self.coupon = #coupon
end
private
def coupon_must_be_active
if #coupon
errors[:coupon] << I18n.t('flash_messages.coupons.id.inactive') unless #coupon.active?
elsif #coupon_code.present?
errors[:coupon_code] << I18n.t('flash_messages.coupons.id.not_found')
end
end
end
It looks like the problem is that there is logic outside of your models that is updating the price_paid column on your Payment, and possibly setting the coupon_id on it as well.
So I would recommend duplicating any extra logic that might be coming from your controllers, service classes, etc. into an after(:create) callback on your factory.
trait :purchased_with_coupon do
# ...other attributes...
association :coupon
after(:create) do |job, evaulator|
discount_value = 100 - job.coupon.percent_discount) / 100.0
calculated_price_paid = job.price * discount_value
create(:payment, price_paid: price_paid, job: job, coupon: coupon)
end
end
Now ultimately, that code belongs in some kind of abstraction, such as a service class that can easily be tested (and used in other tests). However, you mentioned you are getting started on a refactor and want passing tests. I think this is a reasonable compromise until you're ready to abstract it. Ultimately, I would do something like this:
class CreatePaymentWithCoupon
attr_reader :job
def initialize(job)
#job = job
end
def call
job.payments.create(coupon: job.coupon, price_paid: discounted_price)
end
private
def discounted_price
discount_value = (100 - job.coupon.percent_discount) / 100.0
job.price * discount_value
end
end
Then, in your specs:
it "calculates discounted price" do
coupon = create(:coupon, percent_discount: 25)
job = create(:job, :purchased_with_coupon, price: 100)
CreatePaymentWithCoupon.new(job).call
expect(job.payments.first.price_paid).to eq(75.0)
end

Passing arguments to Faker for my method in Rails?

I have a model called Booking, that should calculate the total from several numbers (amount, deposit, and fee are all added together). I'm having trouble getting these arguments to be seen in Faker.
it "should calculate the total" do
myvar = FactoryGirl.create(:booking, :amount => 900, :deposit => 20, :fee => 8)
myvar.totalamount.should == 928
end
And here's my method:
class Booking < ActiveRecord::Base
validates :to, :from, :amount, presence: true
def totalamount(amount,deposit,fee)
total = (amount + deposit + fee)
return total
end
end
The error message: "wrong number of arguments (0 for 3)"
However, when I do a puts myvar.deposit, it returns the value I gave it - 20.
What am I doing wrong?
Edit: Here is my Factory build for Booking:
FactoryGirl.define do
factory :booking do |b|
b.from { Faker::Lorem.sentence(word_count=3) }
b.to { Faker::Lorem.sentence(word_count=3) }
b.amount { Faker::Number.digit }
end
end
class Booking < ActiveRecord::Base
validates :to, :from, :amount, presence: true
def totalamount
total = (amount + deposit + fee)
return total
end
end
Just had to remove the 3 required attributes after 'totalamount'.

How to add in controller in Rails?

How exactly do you do arithmetic operations in the controller?
I've tried this
def choose
rand_id = rand(Gif.count)
#gif1 = Gif.first(:conditions => [ "id >= ?", rand_id])
#gif2 = Gif.first(:conditions => [ "id >= ?", rand_id])
if #gif1.id == #gif2.id
#gif2 = Gif.first(:order => 'Random()')
end
total = #gif1.votes+#gif2.votes
number_one = #gif1.votes/total*100
number_two = #gif2.votes/total*100
#gif1.update_attribute(:votes, number_one)
#gif2.update_attribute(:votes, number_two)
end
class Gif < ActiveRecord::Base
before_save :default_agree_count
def default_agree_count
self.agree = 1
self.votes = 1
end
VALID_REGEX = /http:\/\/[\S]*\.gif$/
attr_accessible :link, :votes, :agree
acts_as_votable
validates :link, presence: true, format: {with: VALID_REGEX}, uniqueness: {case_sensitive: false}
end
However, it says that +, /, * are all unknown operators. I've also tried doing them within like such #gif1.agree = '#gif1.votes+1' with and without '. Any ideas?
Thanks!
I suppose you are using Acts As Votable gem.
Basically it works as follows:
#post = Post.new(:name => 'my post!')
#post.save
#post.liked_by #user
#post.votes.size # => 1
So try replacing .votes with .votes.size in your code.
E.g.:
total = #gif1.votes.size + #gif2.votes.size
Further to #ilyai's answer (which I +1'd) (I don't have much experience with the Acts As Votable gem), you can perform any calculations you want in your controllers
Here's some refactoring for you:
.first
def choose
Gif.update_votes
end
class Gif < ActiveRecord::Base
before_save :default_agree_count
def default_agree_count
self.agree = 1
self.votes = 1
end
def self.update_votes
rand_id = rand count #-> self.count?
gif = where("id >= ?", rand_id)
gif1 = gif[0]
gif2 = gif[1]
if gif1.id == gif2.id
gif2 = where(order: 'Random()').first
end
total = (gif1.votes) + (gif2.votes)
number_one = ((gif1.votes /total) * 100)
number_two = ((gif2.votes / total) * 100)
gif1.update_attribute(:votes, number_one)
gif2.update_attribute(:votes, number_two)
end
VALID_REGEX = /http:\/\/[\S]*\.gif$/
attr_accessible :link, :votes, :agree
acts_as_votable
validates :link, presence: true, format: {with: VALID_REGEX}, uniqueness: {case_sensitive: false}
end

NoMethodError: undefined method `*' for nil:NilClass Factory Girl/Capybara issue

This particular test is trying to create a 'status update' for a user.
Here is the full error:
Failure/Error: FactoryGirl.create(:status_update, user: #user, created_at: 1.day.ago)
NoMethodError:
undefined method `*' for nil:NilClass
# ./app/models/status_update.rb:31:in `default_values'
# ./spec/models/user_spec.rb:28:in `block (3 levels) in <top (required)>'
Here is the test:
describe "Status Update Associations" do
before { #user.save }
let!(:older_status_update) do
FactoryGirl.create(:status_update, user: #user, created_at: 1.day.ago)
end
let!(:newer_status_update) do
FactoryGirl.create(:status_update, user: #user, created_at: 1.hour.ago )
end
it "should have status updates in the right order" do
#user.status_update.should == [newer_status_update, older_status_update]
end
end
Since the error is pointing to the status update model I might as well include that here as well. I suspect it's got something to do with some variables being set after initialization and the let! in the test, although I'm stumped with trying different callbacks.
class StatusUpdate < ActiveRecord::Base
belongs_to :user
after_initialize :default_values
attr_accessible :current_weight,
:current_bf_pct,
:current_lbm,
:current_fat_weight,
:change_in_weight,
:change_in_bf_pct,
:change_in_lbm,
:change_in_fat_weight,
:total_weight_change,
:total_bf_pct_change,
:total_lbm_change,
:total_fat_change
validates :user_id, presence: true
validates :current_bf_pct, presence: true,
numericality: true,
length: { minimum: 4, maximum:5 }
validates :current_weight, presence: true,
numericality: true,
length: { minimum: 4, maximum:5 }
validates :current_lbm, presence: true
validates :current_fat_weight, presence: true
def default_values
self.current_fat_weight = self.current_weight * self.current_bf_pct
self.current_lbm = self.current_weight - self.current_fat_weight
end
default_scope order: 'status_update.created_at DESC'
end
Here is the factory that adds the 'current_weight and current_bf_pct to the default_values method.
factory :status_update do
user
current_weight 150
current_bf_pct 0.15
end
Thanks!
It's due to your default_values method.
You're doing self.current_weight * self.current_bf_pct but none of them are set to a numerical value.

Resources