rspec testing callback worked - ruby-on-rails

i am trying to test some callback before_save logic. But i am stack in this dark place where i cant figure out solution.
I have this method which updates some attributes before save:
def order_item_positions
Place.item_positions_reorder(item_position, city_id).each do |place|
new_item_position = place.item_position + 1
place.update_attributes(item_position: new_item_position)
end
end
What that method does, is changes all records above to +1 position!
and than i want to test it using rspec something like this:
describe "places order" do
let(:city){FactoryGirl.create(:city)}
let(:place){FactoryGirl.create(:place, city_id: city.id)}
let(:place_sec){FactoryGirl.create(:place, city_id: city.id)}
context "when placed before other record" do
it "should be placed before" do
place_sec.item_position = 1
place.item_position = 1
expect{
...somehow saving and triggering callback! //dont know how to do that :/
}.to change(place_sec, :item_position).from(1).to(2)
end
end
end
Any help would be much appreciated! :)

You should build model and then save it, i think:
describe "places order" do
let!(:city) { FactoryGirl.create(:city) }
let!(:place) { FactoryGirl.create(:place, city_id: city.id) }
let!(:place_sec) { FactoryGirl.build(:place, city_id: city.id) }
context "when placed before other record" do
it "should be placed before" do
place_sec.item_position = 1
place.item_position = 1
expect(place_sec.save).to change(place_sec, :item_position).from(1).to(2)
end
end
end
You didn't mention in what model you have this before_save method order_item_positions. So what should you save to call it. Just build this model, and then save.

A simple call to .save should do it:
expect{
place_sec.save
}.to change(place_sec, :item_position).from(1).to(2)

Related

How to test an if-condition not been excuted?

I'm new to Rspec. I have a code like this:
if #context.persona == :system && #context.scopes&.include?(SEARCH_SCOPE)
return <something>
end
I want to write a unit test to confirm the #context.scopes&.include?(SEARCH_SCOPE) is not being executed when #context.persona is not :system. Here is what I wrote:
context 'when persona is system' do
let(:persona) { :system }
it 'checks the scope' do
allow(context).to receive(:scopes)
expect(context).to have_received(:scopes)
end
end
context 'when persona is not system' do
let(:persona) { :user }
it 'checks the scope' do
allow(context).to receive(:scopes)
expect(context).not_to have_received(:scopes)
end
end
The second test passed, but the first test failed with:
Failure/Error: expect(context).to have_received(:scopes)
(Double (anonymous)).scopes(*(any args))
expected: 1 time with any arguments
received: 0 times with any arguments
Could someone help me? I googled it before but didn't see anything helpful. I'm sorry if it is duplicated.
Not a direct answer to your question, but you are falling into the pit of testing implementation, instead of behaviour. Don't do that.
Your test shouldn't care about this:
expect(context).not_to have_received(:scopes)
Instead, your test should only be doing something like this:
context 'when persona is system and scopes includes SEARCH_SCOPE' do
let(:persona) { :system }
let(:scopes) { ... }
it 'returns <something>' do
expect(the_method_being_invoked).to eq(<something>)
end
end
context 'when persona is not system' do
let(:persona) { :user }
let(:scopes) { ... }
it 'returns <something-else>' do
expect(the_method_being_invoked).to eq(<something-else>)
end
end
context 'when scopes is empty' do
let(:persona) { :user }
let(:scopes) { nil }
it 'returns <something-else>' do
expect(the_method_being_invoked).to eq(<something-else>)
end
end
Why? Because when you refactor code, and the implementation changes, you don't want specs to start failing unless the behaviour has also changed.
You should usually even be able to write the test before writing the method -- therefore having no knowledge of its implementation details.

expect{ subject }.to change(...).by(1) doesn't work

This test passes:
it 'test' do
old = parenthood.student.balance
Payment.money_transfer_to_child current_user: parenthood.user,
student_id: parenthood.student.id,
amount: '1',
comment: 'some comment'
expect(parenthood.student.reload.balance).to eq(old+1)
end
changing it to the following form makes it not pass ("was changed by 0"):
subject { Payment.money_transfer_to_child current_user: parenthood.user,
student_id: parenthood.student.id,
amount: '1',
comment: 'some comment' }
it { expect{ subject }.to change(parenthood.student.reload, :balance).by(1) }
What am I doing wrong?
I see that you figured out a way to make it work but maybe let's have a proper answer here. So first why it didn't work.
it { expect{ subject }.to change(parenthood.student.reload, :balance).by(1) }
When this line is executed what is done is (not exactly but roughly):
First the parameter of the change method are calculated. Which means that parenthood.student.reload is evaluated at that point.
The balance method is called on the object that was evaluated, and the value is remembered.
The block in expect (in this case only subject) is executed
Again the balance method is called on the object
The problem here is that parenthood.student.reload was evaluated before the subject was executed. After the execution of subject it was never calculated again as that was already passed to change method. That's why you were having the old value there.
What you can do is either have your solution (but it's not very elegant cause reloading parenthood is not part of what you are testing). Or you can use the block in change matcher and do something like:
it { expect{ subject }.to change { parenthood.student.reload.balance }.by(1) }
That syntax makes the block with parenthood.student.reload.balance evaluated both before running subject and after and should fix your test.
Adding one line to the subject solved my problem:
subject { Payment.money_transfer_to_child current_user: parenthood.user,
student_id: parenthood.student.id,
amount: '1',
comment: 'some comment'
parenthood.student.reload }

Rspec failure - validation

When I run the following command rspec spec/models/vote_spec.rb, I get the following error:
Failures:
1) Vote validations value validation only allows -1 or 1 as values
Failure/Error: expect ( #post.votes ).to eq(-1)
NoMethodError:
undefined method `votes' for nil:NilClass
# ./spec/models/vote_spec.rb:7:in `block (4 levels) in <top (required)>'
Finished in 0.00255 seconds (files took 2.37 seconds to load)
1 example, 1 failure
Here's my code for vote_spec.rb
require 'rails_helper'
describe Vote do
describe "validations" do
describe "value validation" do
it "only allows -1 or 1 as values" do
expect ( #post.votes ).to eq(-1)
expect ( #post.votes ).to eq(1)
end
end
end
end
Sorry I am new to this, I guess my #post variable is not being set. Where should I look for this?
Correct. You're running into this error because your #post variable is nil. What do you mean by "where should I look for this?"
In order to fix this error, you need to define #post somehow in your spec, above the two "examples" in your it block. (This could go in the it block, or in a describe or let block above the it). Two options. Create the object long-form:
#post = Post.create(attribute_hash_here)
or use a factory of some sort (example below uses FactoryGirl):
#post = create(:post)
As it stands, however, were you to do that, your spec would still fail, because it has contrasting expectations:
expect ( #post.votes ).to eq(-1)
expect ( #post.votes ).to eq(1)
Unless the votes method on Post both returns a value AND alters that value, #post.votes will equal EITHER -1 or 1. So if it passes the first expectation, it will fail the second, and if it passes the second, it will fail the first.
** EDIT ** -- As ChrisBarthol pointed out, it's not clear why you need #post to exist at all. If you're just testing a vote's attribute validations, why not just test that object on its own?
First off these are model validations, and you are validating the vote model not the post model, so you should be setting #vote, and not #post. Secondly your test says you expect the value to equal -1 and then 1. How could it be both at the same time? Where are you setting the value such that you expect it? You have to restructure you tests so you are only testing one item at a time.
require 'rails_helper'
describe Vote do
let(:post) { Post.new(whatever post params) }
before { #vote=post.votes.build(whatever vote parameters you have) }
subject { #vote }
describe "validations" do
describe "+1 value valdiation" do
before { #vote.value = 1 }
it { should be_valid }
end
describe "-1 value valdiation" do
before { #vote.value = -1 }
it { should be_valid }
end
describe "other value valdiation" do
before { #vote.value = 0 }
it { should_not be_valid }
end
end
end
I'm guessing at your relationships. There are also better ways to write these tests but that should lead you down the right road.

How Would You Generate Hash with Nested Attributes for Rspec Controller #create Test

I am testing Rails 4 controller #create action in Rspec as following:
describe "POST #create" do
it "saves personality test responses in database" do
expect {
post :create, graduate_id: graduate, personality_test: attributes_for(...)
}.to change(PersonalityTest, :count).by(1)
end
end
Note, the attributes_for(...) above is the part I am struggling with. In order for the controller to create a new record, I need to pass a hash in this format (where "personality_test_template_question_id" and "personality_test_template_answer_id" are foreign keys to existing models:
"personality_test"=>{
"personality_test_template_id"=>"1", "personality_test_responses_attributes"=>{
"0"=>{"personality_test_template_question_id"=>"1", "personality_test_template_answer_id"=>"2"},
"1"=>{"personality_test_template_question_id"=>"2", "personality_test_template_answer_id"=>"9"},
"2"=>{"personality_test_template_question_id"=>"3", "personality_test_template_answer_id"=>"14"},
"3"=>{"personality_test_template_question_id"=>"4", "personality_test_template_answer_id"=>"21"},
"4"=>{"personality_test_template_question_id"=>"5", "personality_test_template_answer_id"=>"27"},
"5"=>{"personality_test_template_question_id"=>"6", "personality_test_template_answer_id"=>"33"},
"6"=>{"personality_test_template_question_id"=>"7", "personality_test_template_answer_id"=>"38"},
"7"=>{"personality_test_template_question_id"=>"8", "personality_test_template_answer_id"=>"46"},
"8"=>{"personality_test_template_question_id"=>"9", "personality_test_template_answer_id"=>"54"},
"9"=>{"personality_test_template_question_id"=>"10", "personality_test_template_answer_id"=>"58"},
"10"=>{"personality_test_template_question_id"=>"11", "personality_test_template_answer_id"=>"64"},
"11"=>{"personality_test_template_question_id"=>"12", "personality_test_template_answer_id"=>"70"},
"12"=>{"personality_test_template_question_id"=>"13", "personality_test_template_answer_id"=>"76"},
"13"=>{"personality_test_template_question_id"=>"14", "personality_test_template_answer_id"=>"83"},
"14"=>{"personality_test_template_question_id"=>"15", "personality_test_template_answer_id"=>"86"},
"15"=>{"personality_test_template_question_id"=>"16", "personality_test_template_answer_id"=>"92"},
"16"=>{"personality_test_template_question_id"=>"17", "personality_test_template_answer_id"=>"101"},
"17"=>{"personality_test_template_question_id"=>"18", "personality_test_template_answer_id"=>"107"},
"18"=>{"personality_test_template_question_id"=>"19", "personality_test_template_answer_id"=>"110"},
"19"=>{"personality_test_template_question_id"=>"20", "personality_test_template_answer_id"=>"119"},
"20"=>{"personality_test_template_question_id"=>"21", "personality_test_template_answer_id"=>"122"},
"21"=>{"personality_test_template_question_id"=>"22", "personality_test_template_answer_id"=>"132"},
"22"=>{"personality_test_template_question_id"=>"23", "personality_test_template_answer_id"=>"137"},
"23"=>{"personality_test_template_question_id"=>"24", "personality_test_template_answer_id"=>"140"},
"24"=>{"personality_test_template_question_id"=>"25", "personality_test_template_answer_id"=>"146"},
"25"=>{"personality_test_template_question_id"=>"26", "personality_test_template_answer_id"=>"152"},
"26"=>{"personality_test_template_question_id"=>"27", "personality_test_template_answer_id"=>"162"},
"27"=>{"personality_test_template_question_id"=>"28", "personality_test_template_answer_id"=>"165"},
"28"=>{"personality_test_template_question_id"=>"29", "personality_test_template_answer_id"=>"173"},
"29"=>{"personality_test_template_question_id"=>"30", "personality_test_template_answer_id"=>"177"}
}
}
I am not clear what would be the proper way to generate a hash in such format. I've tried FactoryGirl attributes_for method, but it doesn't generate attributes for the nested factories. Any tips will be highly appreciated.
Thanks!
I was able to reproduce this type of hash format by manually assembling attributes as following:
describe "POST #create" do
let(:valid_attributes) do
attributes_for(:personality_test).merge(personality_test_template_id: 1,
personality_test_responses_attributes: attributes_for_list(:personality_test_response, 30)
)
end
it "saves personality test responses in database" do
expect{
post :create, graduate_id: graduate, personality_test: valid_attributes
}.to change(PersonalityTest, :count).by(1)
end
end
These are my factories:
FactoryGirl.define do
factory :personality_test do
ignore do
questions_count 30
end
after(:build) do |pt, evaluator|
pt.personality_test_template = PersonalityTestTemplate.find(1)
pt.personality_test_responses = build_list(:personality_test_response,
evaluator.questions_count, personality_test: pt)
end
end
factory :personality_test_response do
personality_test
sequence(:personality_test_template_question_id)
sequence(:personality_test_template_answer_id, 1) { |n| n*6-3}
end
end
Please note, that in this case the parameter hash coming in the request header from RSpec slightly differs from the original one coming from the app view itself in a sense, that RSpec doesn't generate a pseudo-array (e.g. { "0" => {...}, "1"=> {...}... }) for nested attributes. Instead, it generates a regular array of hash attributess and it still works the same. This is what I get from RSpec:
"personality_test"=>{
"personality_test_template_id"=>"1", "personality_test_responses_attributes"=>[
{"personality_test_template_question_id"=>"1", "personality_test_template_answer_id"=>"3"},
{"personality_test_template_question_id"=>"2", "personality_test_template_answer_id"=>"9"},
... (skipped for brevity)
{"personality_test_template_question_id"=>"30", "personality_test_template_answer_id"=>"177"}
]
}
Hope this helps someone.

Contexts in rspec

I have a basic doubt.
If the rspec file contains many contexts:
describe Name do
context "number1" do
.......
.......
end
context "number 2" do
.......
.......
end
context "number 3" do
.......
.......
end
How should the functions from each of the contexts be described in the .rb file? Should they be in the same class or different class? Is there any book I can read to improve my knowledge about this?
The structure I use when defining rspec files (based on reading I've done on rspec) is that you use describes to describe specific functions, and context to talk about a specific context of state and/or path through the function.
Example class:
class MyClass
def self.my_class_method(bool)
if bool == true
return "Yes"
else
return "No"
end
end
def my_instance_method
today = Date.today
if today.month == 2 and today.day == 14
puts "Valentine's Day"
else
puts "Other"
end
end
end
As you can see, I've defined a class method and an instance method that do really silly and random functions. But the point is this: the class method will do something different based on the argument, and the instance method will do something different based on some outside factor: you need to test all these, and these are different contexts. But we will describe the functions in the rspec file.
Rspec file:
describe MyClass do
describe ".my_class_method" do
context "with a 'true' argument" do
it "returns 'Yes'." do
MyClass.my_class_method(true).should eq "Yes"
end
end
context "with a 'false' argument" do
it "returns 'No'." do
MyClass.my_class_method(false).should eq "No"
end
end
end
describe "#my_instance_method" do
context "on Feb 14" do
it "returns 'Valentine's Day'." do
Date.stub(:today) { Date.new(2012,2,14) }
MyClass.new.my_instance_method.should eq "Valentine's Day"
end
end
context "on a day that isn't Feb 14" do
it "returns 'Other'." do
Date.stub(:today) { Date.new(2012,2,15) }
MyClass.new.my_instance_method.should eq "Other"
end
end
end
end
So you can see the describe is for saying what method you're describing, and matches up with the name of a method in your class. The context is used to evaluate different conditions the method can be called in, or different states that affect the way the method works.
Hope this helps!

Resources