Rspec test case for model - ruby-on-rails

I have line_item.rb
class LineItem < ApplicationRecord
belongs_to :product, optional: true
belongs_to :cart
belongs_to :order, optional: true
def total_price
product.price * quantity.to_i
end
end
test case written
require 'rails_helper'
RSpec.describe LineItem, type: :model do
describe '#total_price' do
let!(:user) { create(:user) }
it 'this is for the total function' do
# product = build(:product)
# lineitem = build(:line_item)
category = create(:category)
product = create(:product, category_id: category.id)
order = create(:order, user_id: user.id, email: user.email)
cart = create(:cart)
line_item = create(:line_item,order_id: order.id,product_id: product.id,cart_id:cart.id)
res = product.price * line_item.quantity.to_i
expect(res.total_price).to eq(10)
end
end
end
I am unable to write the test case for total_price. Could anyone let me know
Thank you

You should call total_price on the LineItem object.
category = create(:category)
product = create(:product,
category_id: category.id,
price: 2000) # in cents
order = create(:order,
user_id: user.id,
email: user.email)
cart = create(:cart)
line_item = create(:line_item,
order_id: order.id,
product_id: product.id,
cart_id:cart.id,
quantity: 2)
expect(line_item.total_price).to eq(4000)
A minor thing. The quantity field on the line_items table should be a number. So, you don't need the superfluous to_i call.
def total_price
product.price * quantity
end

Related

Method in model is ignored by test rspec rails

I have a method in my model that group items by variant id if multiple items have the same variant_id she merge them and add their quantity.
My model:
class ShoppingCart < ApplicationRecord
belongs_to :company
belongs_to :user
has_many :items, class_name: "ShoppingCartItem", dependent: :destroy
accepts_nested_attributes_for :items, reject_if: proc { |attributes| attributes['quantity'].blank? }
before_validation :remove_invalid_items
before_validation :merge_items
def merge_items
self.items = items.group_by { |i| i[:variant_id] }.map do |variant_id, is|
quantity_sum = is.sum { |i| i[:quantity] }
ShoppingCartItem.new(variant_id: variant_id, quantity: quantity_sum)
end
end
end
This method works well when i try it manually but in my tests rspec seems to ignore this method
My tests:
require 'rails_helper'
RSpec.describe ShoppingCart, type: :model do
describe "associations" do
it { is_expected.to belong_to(:company) }
it { is_expected.to belong_to(:user) }
it { is_expected.to have_many(:items) }
end
describe "merge_items" do
let(:shopping_cart) { create(:shopping_cart) }
it "merge items if same variant_id" do
existing_item = create(:shopping_cart_item, shopping_cart: shopping_cart, variant_id: "same variant_id", quantity: 1)
item = create(:shopping_cart_item, shopping_cart: shopping_cart, variant_id: "same variant_id", quantity: 1)
expect(shopping_cart.reload.items.count).to eq(1)
end
it "not merge items if variant_id different" do
existing_item = create(:shopping_cart_item, shopping_cart: shopping_cart, variant_id: "variant_id", quantity: 1)
item = create(:shopping_cart_item, shopping_cart: shopping_cart, variant_id: "different variant_id", quantity: 1)
expect(shopping_cart.reload.items.count).to eq(2)
end
end
end
Tests output:
Failure/Error: expect(shopping_cart.reload.items.count).to eq(1)
expected: 1
got: 2
I changed the code and this works:
require 'rails_helper'
RSpec.describe ShoppingCart, type: :model do
describe "associations" do
it { is_expected.to belong_to(:company) }
it { is_expected.to belong_to(:user) }
it { is_expected.to have_many(:items) }
end
describe "merge_items" do
let(:shopping_cart) { create(:shopping_cart) }
context "same variant_id" do
let(:params) {{ shopping_cart: {items_attributes: [{variant_id: 'same variant_id', quantity: 2}, {variant_id: 'same variant_id', quantity: 2}]}}}
before do
shopping_cart.update params[:shopping_cart]
end
it "create just one item" do
expect(shopping_cart.reload.items.count).to eq(1)
end
it "adds all quantities" do
expect(shopping_cart.reload.items.last.quantity).to eq(4)
end
end
context "not same variant_id" do
let(:params) {{ shopping_cart: {items_attributes: [{variant_id: 'variant_id', quantity: 2}, {variant_id: 'different variant_id', quantity: 2}]}}}
before do
shopping_cart.update params[:shopping_cart]
end
it "not merge items if variant_id different" do
expect(shopping_cart.reload.items.count).to eq(2)
end
end
end
end

Rails scope filter/search from association

I have a one-to-one association between PublicKey and PublicKeyRole. The PublicKey model has a column, role_id, and the PublicKeyRole model has role column which is a string, such as 'super'.
I want to be able to search by role string via a url query param, like; https://api.domain.com/public_keys?role=admin. I've tried this on the PublicKey model but I'm not sure where to pass the query in to:
scope :filter_by_role, lambda { |query|
joins(:public_key_role).merge(PublicKeyRole.role)
}
Here are my models:
class PublicKey < ActiveRecord::Base
belongs_to :user
belongs_to :public_key_role, foreign_key: :role_id
def self.search(params = {})
public_keys = params[:public_key_ids].present? ? PublicKey.where(id: params[:public_key_ids]) : PublicKey.all
public_keys = public_keys.filter_by_role(params[:role]) if params[:role]
public_keys
end
end
class PublicKeyRole < ActiveRecord::Base
has_one :public_key
end
Also, here's my test:
describe '.filter_by_role' do
before(:each) do
#public_key1 = FactoryGirl.create :public_key, { role_id: 1 }
#public_key2 = FactoryGirl.create :public_key, { role_id: 2 }
#public_key3 = FactoryGirl.create :public_key, { role_id: 1 }
end
context "when a 'super' role is sent" do
it 'returns the 2 matching public keys results' do
expect(PublicKey.filter_by_role('super').size).to eq(2)
end
it 'returns the matching public keys' do
expect(PublicKey.filter_by_role('super').sort).to match_array([#public_key1, #public_key3])
end
end
end
Update
I was missing the following in my spec, #lcguida's answer works.
FactoryGirl.create :public_key_role, { id: 1, role: 'super' }
From the docs you have some examples. You can search in the relation passing the query to the PublicKeyRole relation:
scope :filter_by_role, lambda { |query|
joins(:public_key_role).merge(PublicKeyRole.where(role: query))
}
If you want a string search (as LIKE):
scope :filter_by_role, lambda { |query|
joins(:public_key_role).merge(PublicKeyRole.where(PublicKeyRole.arel_table[:role].matches(query)))
}

accepts_nested_attributes_for not triggering callback on children?

I have a model called 'Shift' Which has many child models each called 'Booking'. When I update shift, the children are not being updated (in the development environment), despite the fact that the tests are passing.
More detail:
In the controller, we run this action to process reviews.
def review
#shift = Shift.find(params[:id])
#shift.start_will_change! # this makes the 'shift' object 'dirty', triggering callbacks.
if #shift.update(review_params)
#shift.update(reviewed: true)
redirect_to employers_shift_path #shift
else
flash[:notice] = "Could not leave review. If this problem persists please contact will#get-rota.com"
redirect_to employers_shift_path #shift
end
end
Below is the test that passes. So far so good.
test "accepts reviews that change the time" do
assert_nil #booking.start_actual
post :review, id: #shift.id, shift: {
bookings_attributes: {
"0"=> {
"rating_of_worker"=>"4",
"feedback_for_worker"=>"Great job",
"edit_times"=>"true",
"start_date"=> (#booking.start + 1.day).to_s,
"start_time_hours"=>(#booking.start - 1.hour).to_s,
"start_time_minutes"=>#booking.start.to_s,
"end_time_hours"=>#booking.ending.to_s,
"end_time_minutes"=>#booking.ending.to_s,
"id"=>#booking.id}
}
}
#booking.reload
assert_equal 4, #booking.rating_of_worker
assert_equal "Great job", #booking.feedback_for_worker
assert_not_nil #booking.start_actual
assert_equal (#booking.start - 1.hour + 1.day), #booking.start_actual
assert_not_nil #booking.ending_actual
assert_equal (#booking.ending + 1.day), #booking.ending_actual
assert_redirected_to employers_shift_path #shift
end
Other info:
Here's booking:
class Booking < ActiveRecord::Base
belongs_to :shift
after_validation :update_actual_start_and_end
attr_accessor :start_time_hours,
:start_time_minutes,
:end_time_hours,
:end_time_minutes,
:edit_times,
:start_date
def update_actual_start_and_end
if edit_times == 'true'
date = DateTime.parse(start_date)
self.start_actual = start.change({ year: date.year,
month: date.month,
day: date.day,
hour: DateTime.parse(start_time_hours).hour,
min: DateTime.parse(start_time_minutes).minute })
self.ending_actual = ending.change({ hour: DateTime.parse(end_time_hours).hour,
min: DateTime.parse(end_time_minutes).minute })
if self.start_actual > self.ending_actual
self.ending_actual += 1.day
end
end
end
end
Here's Shift:
class Shift < ActiveRecord::Base
has_many :bookings, dependent: :destroy
accepts_nested_attributes_for :bookings
end

RSPEC test NAME ERROR - Undefined Local variable or method

I'm a beginner in ruby on rails and programming in general.
I have an assignment where I have to test my rspec model Vote, and as per instructions the test should pass.
When I run rspec spec/models/vote_spec.rb on the console, I receive the following error:
.F
Failures:
1) Vote after_save calls `Post#update_rank` after save
Failure/Error: post = associated_post
NameError:
undefined local variable or method `associated_post' for #<RSpec::ExampleGroups::Vote::AfterSave:0x007f9416c791e0>
# ./spec/models/vote_spec.rb:22:in `block (3 levels) in <top (required)>'
Finished in 0.28533 seconds (files took 2.55 seconds to load)
2 examples, 1 failure
Failed examples:
rspec ./spec/models/vote_spec.rb:21 # Vote after_save calls `Post#update_rank` after save
Here is my vote_spec code:
require 'rails_helper'
describe Vote do
describe "validations" do
describe "value validation" do
it "only allows -1 or 1 as values" do
up_vote = Vote.new(value: 1)
expect(up_vote.valid?).to eq(true)
down_vote = Vote.new(value: -1)
expect(down_vote.valid?).to eq(true)
invalid_vote = Vote.new(value: 2)
expect(invalid_vote.valid?).to eq(false)
end
end
end
describe 'after_save' do
it "calls `Post#update_rank` after save" do
post = associated_post
vote = Vote.new(value: 1, post: post)
expect(post).to receive(:update_rank)
vote.save
end
end
end
And here is my post_spec code:
require 'rails_helper'
describe Post do
describe "vote method" do
before do
user = User.create
topic = Topic.create
#post = associated_post
3.times { #post.votes.create(value: 1) }
2.times { #post.votes.create(value: -1) }
end
describe '#up_votes' do
it "counts the number of votes with value = 1" do
expect( #post.up_votes ).to eq(3)
end
end
describe '#down_votes' do
it "counts the number of votes with value = -1" do
expect( #post.down_votes ).to eq(2)
end
end
describe '#points' do
it "returns the sum of all down and up votes" do
expect( #post.points).to eq(1) # 3 - 2
end
end
end
describe '#create_vote' do
it "generates an up-vote when explicitly called" do
post = associated_post
expect(post.up_votes ).to eq(0)
post.create_vote
expect( post.up_votes).to eq(1)
end
end
end
def associated_post(options = {})
post_options = {
title: 'Post title',
body: 'Post bodies must be pretty long.',
topic: Topic.create(name: 'Topic name',description: 'the description of a topic must be long'),
user: authenticated_user
}.merge(options)
Post.create(post_options)
end
def authenticated_user(options = {})
user_options = { email: "email#{rand}#fake.com", password: 'password'}.merge(options)
user = User.new( user_options)
user.skip_confirmation!
user.save
user
end
I'm not sure if providing the Post and Vote models code is necessary.
Here is my Post model:
class Post < ActiveRecord::Base
has_many :votes, dependent: :destroy
has_many :comments, dependent: :destroy
belongs_to :user
belongs_to :topic
default_scope { order('rank DESC')}
validates :title, length: { minimum: 5 }, presence: true
validates :body, length: { minimum: 20 }, presence: true
validates :user, presence: true
validates :topic, presence: true
def up_votes
votes.where(value: 1).count
end
def down_votes
votes.where(value: -1).count
end
def points
votes.sum(:value)
end
def update_rank
age_in_days = ( created_at - Time.new(1970,1,1)) / (60 * 60 * 24)
new_rank = points + age_in_days
update_attribute(:rank, new_rank)
end
def create_vote
user.votes.create(value: 1, post: self)
# user.votes.create(value: 1, post: self)
# self.user.votes.create(value: 1, post: self)
# votes.create(value: 1, user: user)
# self.votes.create(value: 1, user: user)
# vote = Vote.create(value: 1, user: user, post: self)
# self.votes << vote
# save
end
end
and the Vote model:
class Vote < ActiveRecord::Base
belongs_to :post
belongs_to :user
validates :value, inclusion: { in: [-1, 1], message: "%{value} is not a valid vote."}
after_save :update_post
def update_post
post.update_rank
end
end
It seems like in the spec vote model, the method assosicated_post can't be retrieved from the post spec model?
You're absolutely right - because you defined the associated post method inside of post_spec.rb, it can't be called from inside vote_spec.rb.
You have a couple options: you can copy your associated post method and put it inside vote_spec.rb, or you can create a spec helper file where you define associated_post once and include it in both vote_spec.rb and post_spec.rb. Hope that helps!

How to Test a Class Method in Rspec that has a Has Many Through Association

How would I test the class method .trending in Rspec considering that it has a has many through association. .trending works but it currently has not been properly vetted in Rspec. Any advice?
class Author < ActiveRecord::Base
has_many :posts
has_many :comments, through: :posts
validates :name, presence: true
validate :name_length
def self.trending
hash = {}
all.each{|x|
hash[x.id] = x.comments.where("comments.created_at >= ?", Time.zone.now - 7.days).count
}
new_hash = hash.sort_by {|k,v| v}.reverse!.to_h
new_hash.delete_if {|k, v| v < 1 }
new_hash.map do |k,v,|
self.find(k)
end
end
private
def name_length
unless name.nil?
if name.length < 2
errors.add(:name, 'must be longer than 1 character')
end
end
end
end
Test I attempted to use (it didn't work)
describe ".trending" do
it "an instance of Author should be able to return trending" do
#author = FactoryGirl.build(:author, name:'drew', created_at: Time.now - 11.years, id: 1)
#post = #author.posts.build(id: 1, body:'hello', subject:'hello agains', created_at: Time.now - 10.years)
#comment1 = #post.comments.build(id: 1, body: 'this is the body', created_at: Time.now - 9.years)
#comment2 = #post.comments.build(id: 2, body: 'this was the body', created_at: Time.now - 8.years)
#comment3 = #post.comments.build(id: 3, body: 'this shall be the body', created_at: Time.now - 7.minutes)
Author.trending.should include(#comment3)
end
end
Neither FactoryGirl.build nor ActiveRecord::Relation#build persists a record to the database—they just return an un-saved instance of the object—but Author.trending is looking for records in the database. You should either call save on the instances to persist them to the database, or use create instead of build.

Resources