the right way to change the associated object in rspec - ruby-on-rails

I recently started to test with rspec, so I can strongly be mistaken, correct me if there is a better way
I create two related models
let(:user) {FactoryGirl.create :user}
let!(:participation) {FactoryGirl.create :participation, user: user}
and before one of the tests change one of the related objects
context "when" do
before {participation.prize = 100}
it "" do
binding.pry
end
end
But inside it
participation.prize => 100
user.participatons.select(:prize) => nil
what am I doing wrong ? and how to fix it?

When you say user.participations.select(:prize), you're making a query to the db to get values in the user's participations' prize columns. But when you say before {participation.prize = 100} you're only setting the prize attribute on the participation object. Try saving the participation before the select line:
participation.prize # => 100
participation.save
user.participatons.select(:prize) # => nil
Another possible issue is that user.participations has been memoized by a previous call. Ensure that user.participations.first == participation. If it doesn't, check
1) puts participation.user_id and
2) puts user.participations, user.reload.participations
Lastly, a better way of setting up the test so that you run into this issue less often is something along the lines of:
# let(:price) { 0 } # default price. Optional so that tests won't throw errors if you forget to set it in a context/describe block.
let(:user) {FactoryGirl.create :user}
let!(:participation) {FactoryGirl.create :participation, user: user, price: price}
# ...
context "when ..." do
let(:price) { 100 }
it "" do
binding.pry
end
end
This way, the price is set when you create the model. Following this pattern generally means running into this problem less.

Related

rspec #variable returning nil

I have an issue with my #attributes variable. I would like it to be accessible to keep my code dry, but currently, I have to restate the variable and set it to "values" to get my rspec test to work. What is a better way to do this without duplicating the values.
ref: Unexpected nil variable in RSpec
Shows that it is not accessible in describe, but there needs be another solution. When would "specify" be appropriate? I have not used it.
describe "When one field is missing invalid " do
before(:each) do
#user = create(:user)
#attributes = {"has_car"=>"true", "has_truck"=>"true", "has_boat"=>"true", "color"=>"blue value", "size"=>"large value"}
end
values = {"has_car"=>"true", "has_truck"=>"true", "has_boat"=>"true", "color"=>"blue value", "size"=>"large value"}
values.keys.each do |f|
p = values.except(f)
it "returns invalid when #{f.to_s} is missing" do
cr = CarRegistration::Vehicle.new(#user, p)
cr.valid?
end
end
end
Update based on comments:
I would also like to use the values array hash in other tests. If I put it in the loop as stated, I would still have to repeat it in other places. Any other recommendations?
Update: I tried using let(),
describe "When one field is missing" do
let(:user) {Factorybot.create(:user)}
let(:attributes) = {{"has_car"=>"true", "has_truck"=>"true", "has_boat"=>"true", "color"=>"blue value", "size"=>"large value"}}
attributes do |f|
p = attributes.except(f)
it "returns invalid when #{f.to_s} is missing" do
cr = CarRegistration::Vehicle.new(user, p)
cr.valid?
end
end
end
but get the following error.
attributes is not available on an example group (e.g. a describe or context block). It is only available from within individual examples (e.g. it blocks) or from constructs that run in the scope of an example (e.g. before, let, etc).
In either of your snippets, you don't need attributes inside of your specs. It is data to generate specs. As such, it must live one level above.
describe "When one field is missing" do
let(:user) { Factorybot.create(:user) }
attributes = { "has_car" => "true", "has_truck" => "true", "has_boat" => "true", "color" => "blue value", "size" => "large value" }
attributes do |f|
p = attributes.except(f)
it "returns invalid when #{f.to_s} is missing" do
cr = CarRegistration::Vehicle.new(user, p)
cr.valid?
end
end
end
As you seem to have recognized, based on the other SO post you linked to, you can't refer to your instance variables out in your describe block. Just set it as a local variable as you've done.
Using let
describe "When one field is missing" do
let(:user) {Factorybot.create(:user)}
let(:attributes) = {{"has_car"=>"true", "has_truck"=>"true", "has_boat"=>"true", "color"=>"blue value", "size"=>"large value"}}
## The variables are used INSIDE the it block.
it "returns invalid when a key is missing" do
attributes do |f|
p = attributes.except(f)
cr = CarRegistration::Vehicle.new(user, p)
expect(cr.valid?).to eq(true) # are you testing the expectation? Added this line.
end
end
end
Personally I don't like writing test (like the above) which could fail for multiple reasons. Sergio is correct. But if you want to use let you have to make use of it from WITHIN the it block - this example shows that.

failure with creating an instance in specs

I have a model Ticket which has department_id, and Department with
enum name: { dept1: 0, dept2: 1, dept3: 2 }
I have seeded db with these three departments
Department.create(name: :dept1)
Department.create(name: :dept2)
Department.create(name: :dept3)
So I try to write specs on Ticket method
def dept
self.department.name.humanize
end
here is an example
describe '.dept' do
let!(:ticket){ create :ticket, department_id: Department.first.id }
it 'should return right dept' do
expect(ticket.dept).to eq 'Dept1'
end
end
And I recieve an error
ActiveRecord::RecordInvalid:
Validation failed: Department can't be blank
I'm a new guy to rails, so please i9f you don't mind explain me how to write such specs( with seeded db). Any advises would be very useful for me. Thanks!
You'll want to refrain from seeding your database and instead create records that you need for each test.
describe '#dept' do
let(:department) { create :department, title: 'dept1' }
let(:ticket) { build :ticket, department: department }
it 'should return right dept' do
expect(ticket.dept).to eq 'Dept1'
end
end
Notice that I also changed ticket so it's generated by build instead of create. Based on what I see, it doesn't look like you need the overhead of persisting ticket to the database in order to run this particular test.
Also, another small point... But the "convention" (if such a thing exists) is to describe instance methods with hashes in front of them instead of a dot. (Dot denotes a class method.)

testing behaviour of method that scopes outputs into a hash

I have a method which creates a key value pair of delivery costs, the key being the type, and the value being the cost.
def calculate_scoped_job_delivery_costs
delivery_hash = {}
['install', 'fuel', 'breakdown'].each do |scope|
delivery_hash[scope.humanize] = job_delivery_costs.send(scope).inject(0) { |total, item| total + (item.cost_per_unit * item.hour_count) * item.quantity }
end
delivery_hash.delete_if {|key, value| value <= 0 }
end
the key is a scope in the job delivery costs model, which retrieves all associated costs with that scope and adds them up. It works, but I want to test its behaviour, albeit retrospectively.
So its core expected behaviour is:
it should output a hash
it should calculate each scope value
it should remove blank values from the hash
So I have written this test (factories posted below)
let(:jdc1){FactoryGirl.create :job_delivery_cost, job: job, delivery_cost: delivery_cost}
let(:jdc2){FactoryGirl.create :job_delivery_cost, job: job, delivery_cost: delivery_cost}
let(:jdc3){FactoryGirl.create :job_delivery_cost, job: job, delivery_cost: delivery_cost}
describe "calculate_scoped_job_delivery_costs" do
before do
allow(jdc1).to receive(:timing).and_return('fuel')
jdc2.update_attributes(quantity: 4)
jdc2.delivery_cost.update_attributes(timing: 'breakdown')
allow(job).to receive(:job_delivery_costs).and_return(JobDeliveryCost.where(id: [jdc1,jdc2,jdc3].map{|jdc| jdc.id}))
end
it "should retrieve a hash with jdc scopes" do
expect(job.calculate_scoped_job_delivery_costs.is_a?(Hash)).to be_truthy
end
it "should calculate each hash value" do
expect(job.calculate_scoped_job_delivery_costs).to eq "Fuel"=>15.0
end
it "should remove blank values from hash" do
expect(job.calculate_scoped_job_delivery_costs).to_not include "Breakdown"=>0
end
end
So in the last test, it passes, why? I have purposefully tried to make it break by updating the attributes in the before block on jdc2 so that breakdown is another scoped value.
Secondly, by changing the state of jdc2 and its values, this should break test 2 as fuel is no longer calculated against the same values.
Here are my factories...
FactoryGirl.define do
factory :job_delivery_cost do
job
delivery_cost
cost_per_unit 1.5
quantity 3
hour_count 1.0
end
end
FactoryGirl.define do
factory :delivery_cost do
title
timing "Fuel"
cost_per_unit 1.5
end
end
FactoryGirl.define do
factory :job do
job_type
initial_contact_id_placeholder {FactoryGirl.create(:contact).id}
title "random Title"
start "2013-10-04 11:21:24"
finish "2013-10-05 11:21:24"
delivery "2013-10-04 11:21:24"
collection "2013-10-05 11:21:24"
delivery_required false
collection_required false
client { Client.first || FactoryGirl.create(:client) }
workflow_state "offer"
admin
end
end
job has_many :job_delivery_costs.
job_delivery_cost belongs_to :delivery_cost
has_many :job_delivery_costs
has_many :jobs, through: :job_delivery_costs
I am really struggling with the logic of these tests, I am sure there are more holes than what I have laid out above. I welcome criticism in that regard.
thanks
A couple of suggestions:
Remember that let is lazy-evaluated; factories within the block are not created until the symbol defined by let is encountered in the code. This can have unexpected consequences for things like scopes, where you might think that the database already includes your factory-generated rows. You can get around this by using let!, which is evaluated immediately, or by restructuring the spec to ensure things get created in the right order.
I prefer not to do partial stubbing where it can be avoided. For scopes, you are probably better off using factories and just letting the scopes retrieve the rows instead of stubbing the relations. In your case this means getting rid of the code in the before block and setting up each example with factories, so that the scopes are retrieving the expected values.

Passing a simple test

I'm using Rails 3.2's rake tests function. I'm trying to pass a test but it's giving me errors. Btw, when see you how I write, I'm a noob. It's a hacked way of testing, but at least I want to try passing it first.
test "product title must have at least 10 characters" do
ok = %w{ aaaaaaaaaa aaaaaaaaaaa }
bad = %w{ a aa aaa aaaa aaaaa aaaaaa aaaaaaa aaaaaaaa aaaaaaaaa}
ok.each do |name|
assert new_product_title(name).valid?, "#{name} shouldn't be invalid"
end
bad.each do |name|
assert new_product_title(name).invalid?, "#{name} shouldn't be valid"
end
end
with the function
def new_product_title(title)
Product.new(title: title,
description: "yyy",
price: 1,
image_url: "fred.gif")
end
somehow it's not passing.
What's the reason here? And is there a better way to write it?
I'm more concerned about the method. I'm assuming this method is in a product model? It seems what you are trying to do should definitely be controlled by the model, but I don't think you can call a class's method inside the class's definition. I also don't see much utility in a method that creates a new product with specified title, but static description, price, and image_url. If you need default values for specific attributes, you can set those in an initialize method and overwrite them later if needed. Some people frown on setting defaults in initialize so instead you can set them in an after_initialize callback like this:
class Product < ActiveRecord::Base
after_initialize :init
def init
self.description ||= 'yyy'
self.price ||= 1
self.image_url ||= "fred.gif"
end
end
Then whenever you needed to create a new product with a title and the default attributes you can just use
Product.new(:title => "some title")
And if you don't want all the defaults you can just pass the values into new like usual
Product.new(:title => "some other title", :price => 400) # desc & url are still default
About your tests. I always test in RSpec. Since you are using Test Unit (or Mini Test or whatever it is now), my advice my not be correct. But first I would make the variable names more descriptive. Secondly, there are some commas at the end of your assertions that shouldn't be there.
test "product title must have at least 10 characters" do
valid_name = "a" * 10
short_name = "a" * 9
valid_product = Product.new(:name => valid_name)
assert valid_product.valid?
invalid_product = Product.new(:name => short_name)
assert invalid_product.invalid?
end
If you get that working you may want to verify that the product is invalid for the correct reason using an assert equals method on invalid_product.errors.full_messages and the expected string from the error.

Moching rails association methods

Here is my helper method which I want to test.
def posts_correlation(name)
if name.present?
author = User.find_by_name(name)
author.posts.count * 100 / Post.count if author
end
end
A factory for user.
factory :user do
email 'user#example.com'
password 'secret'
password_confirmation { password }
name 'Brian'
end
And finally a test which permanently fails.
test "should calculate posts count correlation" do
#author = FactoryGirl.create(:user, name: 'Jason')
#author.posts.expects(:count).returns(40)
Post.expects(:count).returns(100)
assert_equal 40, posts_correlation('Jason')
end
Like this.
UsersHelperTest:
FAIL should calculate posts count correlation (0.42s)
<40> expected but was <0>.
test/unit/helpers/users_helper_test.rb:11:in `block in <class:UsersHelperTest>'
And the whole problem is that mocha doesn't really mock the count value of author's posts, and it returns 0 instead of 40.
Are there any better ways of doing this: #author.posts.expects(:count).returns(40) ?
When your helper method runs, it's retrieving its own object reference to your author, not the #author defined in the test. If you were to puts #author.object_id and puts author.object_id in the helper method, you would see this problem.
A better way is to pass the setup data for the author in to your mocked record as opposed to setting up expectations on the test object.
It's been a while since I used FactoryGirl, but I think something like this should work:
#author = FactoryGirl.create(:user, name: 'Jason')
(1..40).each { |i| FactoryGirl.create(:post, user_id: #author.id ) }
Not terribly efficient, but should at least get the desired result in that the data will actually be attached to the record.

Resources