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
Related
I am testing model using rspec and factory Girl
My model
class Currency < ActiveRecord::Base
has_many :countries
validates :name, presence: true
validates :name, uniqueness: true
before_destroy :safe_to_delete
def safe_to_delete
countries.any? ? false : true
end
end
My factory girl
FactoryGirl.define do
factory :currency, class: 'Currency' do
sequence(:name) { |i| "Currency-#{i}" }
end
end
My currency_spec.rb is
require 'rails_helper'
describe Currency , type: :model do
let(:currency) { create(:currency) }
let(:currency1) { create(:currency) }
let(:country) { create(:country) }
describe 'associations' do
subject {currency}
it { should have_many(:countries) }
end
describe 'validations' do
subject {currency}
it { should validate_presence_of(:name) }
it { should validate_uniqueness_of(:name) }
end
describe 'method save_to_delete' do
context 'case false' do
before { country.update_column(:currency_id, currency.id) }
subject { currency.destroy }
it { is_expected.to be_falsy }
end
context 'case true' do
before { country.update_column(:currency_id, currency1.id) }
subject { currency.destroy }
it { is_expected.to be_truthy }
end
end
end
The error is:
Failure/Error: let(:currency) { create(:currency) }
ActiveRecord::RecordInvalid:
A validação falhou: Name não está disponível
Even though I disable the presence and uniqueness validations in the model, the problem continues
Who can help me
Did you properly create the migration to include the name on currencies at database level?
Because I created the migration here locally and the tests passed.
Please take a look on the below code.
It is what I did locally and is working here!
1. Migration
file: db/migrate/2021XXXXXXXXXX_create_currencies.rb
class CreateCurrencies < ActiveRecord::Migration
def change
create_table :currencies do |t|
t.string :name
end
end
end
2. Model
app/models/currency.rb
class Currency < ActiveRecord::Base
has_many :countries
validates :name, presence: true, uniqueness: true # Can be oneliner ;)
before_destroy :safe_to_delete
def safe_to_delete
countries.empty? # Much simpler, right? ;)
end
end
3. Factory
spec/factories/currency.rb
FactoryGirl.define do
factory :currency do
sequence(:name) { |i| "Currency-#{i}" }
end
end
4. Tests
spec/models/currency_spec.rb
require 'rails_helper'
describe Currency, type: :model do
let(:currency1) { create(:currency) }
let(:currency2) { create(:currency) }
let(:country) { create(:country) }
describe 'associations' do
subject { currency1 }
it { should have_many(:countries) }
end
describe 'validations' do
subject { currency1 }
it { should validate_presence_of(:name) }
it { should validate_uniqueness_of(:name) }
end
describe 'when the currency is being deleted' do
context 'with countries associated' do
before { country.update_column(:currency_id, currency1.id) }
subject { currency1.destroy }
it { is_expected.to be_falsy }
end
context 'with no countries associated' do
before { country.update_column(:currency_id, currency2.id) }
subject { currency1.destroy }
it { is_expected.to be_truthy }
end
end
end
Test Execution
Finally, the tests should work correctly with the above setup!
spec/models/currency_spec.rb
rspec spec/models/currency_spec.rb
D, [2021-03-06T03:31:03.446070 #4877] DEBUG -- : using default configuration
D, [2021-03-06T03:31:03.449482 #4877] DEBUG -- : Coverband: Starting background reporting
.....
Top 5 slowest examples (0.10688 seconds, 11.4% of total time):
Currency when the currency is being deleted with countries associated should be falsy
0.04095 seconds ./spec/models/currency_spec.rb:23
Currency associations should have many countries
0.03529 seconds ./spec/models/currency_spec.rb:10
Currency when the currency is being deleted with no countries associated should be truthy
0.01454 seconds ./spec/models/currency_spec.rb:29
Currency validations should validate that :name cannot be empty/falsy
0.00812 seconds ./spec/models/currency_spec.rb:15
Currency validations should validate that :name is case-sensitively unique
0.00797 seconds ./spec/models/currency_spec.rb:16
Finished in 0.93948 seconds (files took 8.04 seconds to load)
5 examples, 0 failures
All tests passed ✅
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
I'm using this factory file for user model:
FactoryBot.define do
factory :user do |f|
f.sequence(:first_name) { |n| "#{Faker::Name.first_name}foo#{n}" }
f.sequence(:last_name) { |n| "#{Faker::Name.last_name}foo#{n}" }
f.sequence(:email) { |n| "foo#{n}#example.com" }
f.password "foobar"
f.password_confirmation { |u| u.password }
f.sequence(:confirmed_at) { Date.today }
f.sequence(:telephone_number) { Faker::Number.number(10) }
f.sequence(:mobile_phone_number) { Faker::Number.number(10) }
f.sequence(:verification_code) { '0000' }
f.sequence(:is_verified) { false }
end
end
and Order.rb factory is:
FactoryBot.define do
factory :order do
association :store
association :user
total_price Faker::Number.positive
total_discount Faker::Number.positive
end
end
And the order model should have these three FKs, two of which are from User:
class Order < ApplicationRecord
belongs_to :customer, class_name: 'User'
belongs_to :carrier, class_name: 'User'
belongs_to :store
end
and in order_controllers_spec.rb file, I got these:
let(:customer) { FactoryBot.create(:user) }
let(:carrier) { FactoryBot.create(:user) }
let(:store) { FactoryBot.create(:store) }
let(:order) { FactoryBot.create(:order, customer_id: customer.id, carrier_id: carrier.id, store_id: store.id) }
Each time I run the show test,
describe "GET show" do
it 'has a 200 status code' do
get :show, params: { id: order_item.id }
expect(response.status).to eq(200)
end
end
I got this error
Failure/Error: let(:order) { FactoryBot.create(:order, customer_id: customer.id, carrier_id: carrier.id, store_id: store.id) }
NoMethodError:
undefined method `user=' for #<Order:0x00007fcd2efc5118>
Any ideas about how to solve this?
I think in your Order's factory definition you're using user, instead of customer or carrier as your Order model define.
association :customer, factory: :user
association :carrier, factory: :user
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!
Here is the method I am testing:
class User < ActiveRecord::Base
has_many :sports, :through => :user_sports, order: "user_sports.created_at", class_name: "Sport"
has_many :user_sports
def primary_sport
return nil if user_sports.blank?
user_sports.primary_only.first.sport
end
end
User Factory;
FactoryGirl.define do
sequence(:email) do |n|
"user#{n}#example.com"
end
factory :user do
email
first_name Faker::Name.first_name
last_name Faker::Name.last_name
password "password"
password_confirmation "password"
agreed_to_age_requirements true
username "testing123"
state "AL"
city_id 201
school_id 20935
handedness "Left"
customer_id { "#{rand(1000)}" }
sports {[create(:sport)]}
after(:create) do |user, elevator|
user.subscriptions << create(:subscription)
user.roles << create(:role)
end
end
factory :athlete, class: "Athlete", parent: :user do
type "Athlete"
recruit_year "2016"
end
end
Here is my test:
require 'spec_helper'
describe User do
describe "associations" do
it { should have_and_belong_to_many(:roles) }
it { should belong_to(:account_type) }
it { should belong_to(:primary_sport).class_name("Sport") }
it { should belong_to(:school) }
it { should belong_to(:city) }
it { should belong_to(:hometown) }
it { should have_many(:social_actions) }
it { should have_one(:invitation) }
it { should have_many(:authorizations) }
it { should belong_to(:user_type) }
it { should have_and_belong_to_many(:positions).class_name "SportPosition" }
it { should have_many(:sports).through(:user_sports) }
it { should have_many(:user_sports) }
it { should have_many :contributorships }
it { should have_many(:managed_athletes).through(:contributorships) }
it { should have_and_belong_to_many(:subscriptions) }
end
describe "nested attributes" do
it { should accept_nested_attributes_for(:user_sports) }
it { should accept_nested_attributes_for(:subscriptions) }
end
describe "validations" do
it { should validate_presence_of(:email) }
it { should validate_uniqueness_of(:email) }
it { should allow_value("test#test.com").for(:email) }
it { should_not allow_value("test.com").for(:email) }
end
describe "instance methods" do
before :each do
#user = create(:user, sports: [])
#school_admin_role = create(:role, name: "School Admin")
#contributor_role = create(:role, name: "Contributor")
end
describe "#my_athletes_path" do
it "returns a school admin path if the user has the role of School Admin" do
#user.roles << #school_admin_role
#user.my_athletes_path.should eq school_admin_athletes_path
end
it "returns a school admin path if the user has the role of Contributor" do
#user.roles << #contributor_role
#user.my_athletes_path.should eq contributor_dashboard_path
end
it "returns nil if the user has no Contributor or School Admin role" do
#user.my_athletes_path.should be_nil
end
end
describe "#first_time_login?" do
it "will evalute true if the user has logged in only once" do
#user.sign_in_count = 1
#user.save
#user.first_time_login?.should be_true
end
end
describe "#confirmation_required?" do
it "returns false" do
#user.confirmation_required?.should be_false
end
end
describe "#primary_sport", focus: true do
context "when user has no primary sport" do
it "returns nil" do
#user.primary_sport.should be_nil
end
end
context "when user has a primary sport" do
it "returns sport object" do
#user.sports << create(:sport)
#user.primary_sport.should eq #user.sports.first
end
end
end
end
end
This is the error I am receiving:
Failure/Error: #user.primary_sport.should eq #user.sports.first
NoMethodError:
undefined method sport for nil:NilClass
This is because when the user_sport association is created in the User Factory, the primary column is being set to false. Not sure how to fix this. Any help is greatly appreciated! Also, sorry for the ignorance on the TDD front, Im a newb
Couldn't you just add the following to your after(:create) block in the User factory:
us = user.user_sports.first
us.primary = true
us.save
That would ensure the association gets the primary flag.