How to create an instance variable with FactoryBot on Rspec - ruby-on-rails

I want to test a class function on RSpec on my Product class. To ease the reading I will keep it as this:
class Product < ApplicationRecord
private
def self.order_list(sort_by)
if sort_by == 'featured' || sort_by.blank?
self.order(sales: :desc)
elsif sort_by == 'name A-Z'
self.order(name: :asc)
elsif sort_by == 'name Z-A'
self.order(name: :desc)
elsif sort_by == 'rating 1-5'
self.order(:rating)
elsif sort_by == 'rating 5-1'
self.order(rating: :desc)
elsif sort_by == 'price low-high'
self.order(:price)
elsif sort_by == 'price high-low'
self.order(price: :desc)
elsif sort_by == 'date old-new'
self.order(:updated_at)
elsif sort_by == 'date new-old'
self.order(updated_at: :desc)
end
end
end
Once the function is called with a parameter, depending on the parameter that has been used, the list of products is ordered in a different way for the user to see.
I built a FactoryBot for the product model as well:
FactoryBot.define do
factory :product do
sequence(:name) { |n| "product_test#{n}" }
description { "Lorem ipsum dolor sit amet" }
price { rand(2000...10000) }
association :user, factory: :non_admin_user
rating { rand(1..5) }
trait :correct_availability do
availability { 1 }
end
trait :incorrect_availability do
availability { 3 }
end
#Adds a test image for product after being created
after(:create) do |product|
product.photos.attach(
io: File.open(Rails.root.join('test', 'fixtures', 'files', 'test.jpg')),
filename: 'test.jpg',
content_type: 'image/jpg'
)
end
factory :correct_product, traits: [:correct_availability]
factory :incorrect_product, traits: [:incorrect_availability]
end
end
Basically, we want to call the :correct_product to create a product thats accepted by the validations of the model.
For the specs:
describe ".order_list" do
let!(:first_product) { FactoryBot.create(:correct_product, name: "First Product" , rating: 1) }
let!(:second_product) { FactoryBot.create(:correct_product, name: "Second Product" , rating: 2) }
let!(:third_product) { FactoryBot.create(:correct_product, name: "Third Product" , rating: 3) }
it "orders products according to param" do
ordered_list = Product.order_list('rating 1-5')
expect(ordered_list.all).to eq[third_product, second_product, first_product]
end
end
So basically, my question is, how can I create an instance variable for each of the 3 mock products so I can name them here in the order I expect them to appear:
expect(ordered_list.all).to eq[third_product, second_product, first_product]
Or, even better, is there a way to create the instance variables with a loop and actually have the instance variable name to use them in the expect? This would free me from having to create 3 different variables on FactoryBot as I did.
I searched around the web and instance_variable_set is used in some cases:
Testing Rails with request specs, instance variables and custom primary keys
Simple instance_variable_set in RSpec does not work, but why not?
But it doesn´t seem to work for me.
Any idea on how could I make it work? Thanks!

Related

How to merge different keys?

I have two Model classes
class Book
# attributes
# title
# status
# author_id
belongs_to :author
enum status: %w[pending published progress]
end
class Author
# attributes
has_many :books
end
I have an activerecord that return a list of books
The list
[<#Book><#Book><#Book>]
I use this function to group them
def group_by_gender_and_status
books.group_by { |book| book.author.gender }
.transform_values { |books| books.group_by(&:status).transform_values(&:count) }
end
and the outcome is
{
"female"=>{"progress"=>2, "pending"=>1, "published"=>2},
"male"=>{"published"=>3, "pending"=>4, "progress"=>4}
}
How do I merge progress and pending and name the key pending? so it would look like this
{
"female"=>{"pending"=>3, "published"=>2 },
"male"=>{"pending"=>8, "published"=>3, }
}
I prefer to use the group_by method vs the group by SQL for a reason. Thanks
def group_by_gender_and_status
books.group_by { |book| book.author.gender }.
transform_values do |books|
books.group_by { |book| book.status == 'progress' ? 'pending' : book.status }.
transform_values(&:count)
end
end

question about Factory Girl syntax for passing an option to a trait

I am taking over a project that has a question / answer section. I am adding a syndication feature and would like to have a relationship where a question has_one: syndicatable_question.
For my factrory, I have an API like sq = FactoryGirl.create(:question, :with_syndication ) for the simple case and would like something like sq = FactoryGirl.create(:question, :with_syndication(syndicatable_location_id: 345)) but this doesn't work. How could I pass an option / argument for a trait? What changes would I need to make to the factory?
My factory currently looks like this:
FactoryGirl.define do
factory :question, class: Content::Question do
specialty_id 2
subject { Faker::Lorem.sentence }
body { Faker::Lorem.paragraph }
location_id 24005
trait :with_syndication do
after(:create) do |q, options|
create(:syndicatable_question, question_id: q.id, syndicatable_location_id: q.location_id)
end
end
end
end
You need to add transient block to your trait
FactoryGirl.define do
factory :question, class: Content::Question do
specialty_id 2
subject { Faker::Lorem.sentence }
body { Faker::Lorem.paragraph }
location_id 24005
transient do
syndicatable_location_id 24005
end
trait :with_syndication do
after(:create) do |q, options|
create(:syndicatable_question, question_id: q.id, syndicatable_location_id: options.syndicatable_location_id)
end
end
end
end
FactoryGirl.create(:question, :with_syndication, syndicatable_location_id: 345)
Transient Attributes
https://www.rubydoc.info/gems/factory_girl/file/GETTING_STARTED.md#Traits

How to sort enum with Ransack?

I've got a table users with enum role set like this:
enum role: {
superadmin: 0,
admin: 1,
user: 2
}
And I want to sort it with Ransack like the rest of the attributes. How can I do this?
Of course, I could sort them alphabetically, but what if I want to add another role in the future?
I made a function based off the answer to https://stackoverflow.com/a/37599787/2055858
def alphabetical_enum_sql(enum, field_name)
ordered_enum = enum.keys.sort
order_by = ["case"]
ordered_enum.each_with_index do |key, idx|
order_by << "WHEN #{field_name}=#{enum[key]} THEN #{idx}"
end
order_by << "end"
order_by.join(" ")
end
and then used it like
scope :order_by_role, -> {
order(alphabetical_enum_sql(YourModel.roles, "role"))
}

write an Rspec test to test multiple nested_attibutes

I am trying to put together a test that will test an object with its nested_attributes.
I have a simple setup
class Animal < ActiveRecord::Base
has_many :animal_images
end
class AnimalImage < ActiveRecord::Base
belongs_to :animal
end
From the factory_girl docs you can create an associated object like so
FactoryGirl.define do
factory :animal, class: Animal do
ignore do
images_count 1
end
after(:create) do |animal, evaluator|
create_list(:animal_image, evaluator.images_count, animal: animal)
end
end
end
FactoryGirl.define do
factory :animal_image do
image { File.open("#{Rails.root}/spec/fixtures/yp2.jpg") }
end
end
so how would i construct a test to look at a custom validation method that validates the number of images..
custom method
def max_num_of_images
if image.size >= 4
errors.add(:base, "Only 3 images allowed")
end
end
But where would i use this, in Animal or AnimalImage model? Am i to assume the AnimalImage model as i have access to the image attribute?
So far i have this
it 'is invalid with 4 images' do
animal = FactoryGirl.create(:animal, images_count: 4)
animal_image = AnimalImage.create!(animal: animal, image: #how do i pass the 4 images i created earlier)
ap(animal)
ap(animal_image)
end
So ap(animal) will return
#<Animal:0x00000001c9bbd8> { :id => 52 }
and ap(animal_image) will return
#<AnimalImage:0x00000005457b30> { :id => 231, :animal_id => 52 }
What i need to do is create 4 animal_images with the same animal_id and have my validation fail it as there are more than 3 images..
Does anyone have any idea on how to go about this please?
Thank You
If i understood right, you can create array of images like:
FactoryGirl.define do
factory :animal do
name 'monkey'
trait :with_image
create_list(:animal_images, 3)
end
trait :with_5_images
create_list(:animal_images, 5)
end
end
end
FactoryGirl.define do
factory :animal_image do
image { File.open("#{Rails.root}/spec/fixtures/yp2.jpg") }
end
end
describe Animal do
subject(:animal) { build :animal, :with_images }
let(:invalid_animal) { build :animal, :with_5_images }
it { expect(subject.valid?).to be_truthy }
it { expect(invalid_aminal.valid?).to be_falsy }
end

Testing after_hook with Rspec

I have tag system and field for auto incrementing questions count, that belongs to tag. I am using Mongoid.
Question model:
class Question
has_and_belongs_to_many :tags, after_add: :increment_tag_count, after_remove: :decrement_tag_count
after_destroy :dec_tags_count
...
private
def dec_tags_count
tags.each do |tag|
tag.dec_questions_count
end
end
and Tag model:
class Tag
field :questions_count, type: Integer,default: 0
has_and_belongs_to_many :questions
def inc_questions_count
inc(questions_count: 1)
end
def dec_questions_count
inc(questions_count: -1)
end
It works fine when I am testing it in browser by hand,it increments and decrements tag.questions_count field when adding or removing tags, but my test for Question model after_destroy hook always fall.
it 'decrement tag count after destroy' do
q = Question.create(title: 'Some question', body: "Some body", tag_list: 'sea')
tag = Tag.where(name: 'Sea').first
tag.questions_count.should == 1
q.destroy
tag.questions_count.should == 0
end
expected: 0
got: 1 (using ==)
it {
#I`ve tried
expect{ q.destroy }.to change(tag, :questions_count).by(-1)
}
#questions_count should have been changed by -1, but was changed by 0
need help...
It's because your tag is still referencing the original Tag.where(name: 'Sea').first. I believe you can use tag.reload after destroying the question (couldn't try this to confirm) like follows:
it 'decrement tag count after destroy' do
...
q.destroy
tag.reload
tag.questions_count.should == 0
end
But better than this is to update your tag to point to q.tags.first, which, I believe is what you want:
it 'decrement tag count after destroy' do
q = Question.create(title: 'Some question', body: "Some body", tag_list: 'sea')
tag = q.tags.first # This is going to be 'sea' tag.
tag.questions_count.should == 1
q.destroy
tag.questions_count.should == 0
end

Resources