Testing after_hook with Rspec - ruby-on-rails

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

Related

How to create an instance variable with FactoryBot on Rspec

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!

save items in an array to active records

I have an array that looks like this:
[{"id"=>"2", "reply"=>"ok"}, {"id"=>"3", "reply"=>"ok"}, {"id"=>"4", "reply"=>"ok"}, {"id"=>"5"}, {"id"=>"6", "reply"=>"2"}]
now I'm trying to save it like this:
current_user.answers.transaction do
success = params[:answers].map(&:save)
unless sucess.all?
errored = params[:answers].select { |b| !b.errors.blank? }
raise ActiveRecord::Rollback
end
end
but that results in
undefined method `save' for {"id"=>"2", "reply"=>"ok"}:ActionController::Parameters
does anybody know how I can save each item?
class Answer < ActiveRecord::Base
belongs_to :user
belongs_to :question
has_many :comments
validates :reply, :question_id, :week_number, presence: true
end
Yeah, just send it a block... so you can try:
current_user.answers.transaction do
success = params[:answers].map do |hash|
object = Model.new(hash)
object.save
end
unless success.all?
errored = params[:answers].select { |b| !b.errors.blank? }
raise ActiveRecord::Rollback
end
end
Realize that Model needs to be the name of the model you're trying to instantiate objects with.
but, that's only if you're making new objects, which generally won't be the case of you've set the id attribute already
current_user.answers.transaction do
success = params[:answers].map do |hash|
object = Model.find(hash["id"])
object.update_attributes(hash)
end
unless success.all?
errored = params[:answers].select { |b| !b.errors.blank? }
raise ActiveRecord::Rollback
end
end
Hope this helps

RSpec test of Rails model class method

I have a method in my model
class Announcement < ActiveRecord::Base
def self.get_announcements
#announcements = Announcement.where("starts <= :start_date and ends >= :end_date and disabled = false",
{:start_date => "#{Date.today}", :end_date => "#{Date.today}"})
return #announcements
end
end
I am trying to write rspec for this method, as i am new to rspec cant proceed
describe ".get_announcements" do
before { #result = FactoryGirl.create(:announcement) }
it "return announcements" do
end
end
Please help
Solution for my question
describe ".get_announcements" do
before { #result = FactoryGirl.create(:announcement) }
it "return announcement" do
Announcement.get_announcements.should_not be_empty
end
end
describe ".get_announcements" do
let!(:announcements) { [FactoryGirl.create!(:announcement)] }
it "returns announcements" do
expect(Announcement.get_announcements).to eq announcements
end
end
Note the use of let! to immediately (not lazily) assign to announcements.
Does the class method really need to define an instance variable? If not, it could be refactored to:
class Announcement < ActiveRecord::Base
def self.get_announcements
Announcement.where("starts <= :start_date and ends >= :end_date and disabled = false",
{:start_date => "#{Date.today}", :end_date => "#{Date.today}"})
end
end

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.

Call a Method Model inside Controller

I have the following model;
(app/models/student_inactivation_log.rb)
class StudentInactivationLog < ActiveRecord::Base
belongs_to :student
belongs_to :institution_user
belongs_to :period
validates_presence_of :student_id, :inactivated_on, :inactivation_reason
INACTIVATION_REASONS = [{ id: 1, short_name: "HTY", name: "You didn't study enough!"},
{ id: 2, short_name: "KS", name: "Graduated!"},
{ id: 3, short_name: "SBK",name: "Other Reason"}]
Class methods
class << self
def inactivation_reason_ids
INACTIVATION_REASONS.collect{|v| v[:id]}
end
def inactivation_reason_names
INACTIVATION_REASONS.collect{|v| v[:name]}
end
def inactivation_reason_name(id)
INACTIVATION_REASONS.select{|t| t[:id] == id}.first[:name]
end
def inactivation_reason_short_name(id)
INACTIVATION_REASONS.select{|t| t[:id] == id}.first[:short_name]
end
def inactivation_reason_id(name)
INACTIVATION_REASONS.select{|t| t[:name] == name}.first[:id]
end
end
# Instance methods
def inactivation_reason_name
self.class.inactivation_reason_name(self.inactivation_reason)
end
def inactivation_reason_short_name
self.class.inactivation_reason_short_name(self.inactivation_reason)
end
def inactivation_reason_id
self.class.inactivation_reason_id(self.inactivation_reason)
end
end
I would like to call these inactivation reasons from my controller, which is app/controllers/student/session_controllers.rb file:
class Student::SessionsController < ApplicationController
layout 'session'
def create
student = Student.authenticate(params[:student_number], params[:password])
if student.active
session[:student_id] = student.id
redirect_to student_main_path, :notice => 'Welcome!'
elsif (student and student.student_status == 3) or (student and !student.active)
flash.now.alert = "You can't login because #REASON_I_AM_TRYING_TO_CALL"
render 'new'
else
....
end
end
I would like to show students their inactivation reason on the systems if they can't login.
How can I call my INACTIVATION_REASONS from this controller file? Is it possible?
Thanks in advance!
That's just a constant, so you can call it as constant anywhere.
StudentInactivationLog::INACTIVATION_REASONS
Update
I realized actually what you want is to use a reason code or short name saved in db to represent the string.
If so, I recommend you to use the short name directly as Hash. "id" looks redundant for this light case.
INACTIVATION_REASONS = {"HTY"=>"You didn't study enough!",
"KS"=>"Graduated!",
"SBK"=>"Other Reason"}
validates :inactivation_reason, inclusion: { in: INACTIVATION_REASONS.keys,
message: "%{value} is not a valid short name" }
def full_reason_message
INACTIVATION_REASONS[self.inactivation_reason]
end
Then, to show full message of a reason in controller
reason = #student.full_reason_message
This is the idea. I havn't checked your other model codes. You'll need to save reason as the short name instead of id, and need to revise/remove some code if you decide to use it in this way.

Resources