I'm using FactoryGirl to create instances of a date dimension model for a Rails-related gem. My factory looks like this:
FactoryGirl.define do
sequence :next_day do |n|
Date.new(2000,12,31) + n.days
end
factory :date_dimension do
the_date = FactoryGirl.generate(:next_day)
date {the_date.to_s}
calendar_year {the_date.strftime("%Y")}
(...other attributes created similarly to calendar_year)
end
end
Out of frustration I actually built a little test to show what's not working:
describe "working date factories" do
before(:all) do
#date_dimension = FactoryGirl.create(:date_dimension)
#jan_two = FactoryGirl.create(:date_dimension)
end
describe "sequence incrementing" do
it "returns a date dimension object ok" do
#date_dimension.date.should == "2001-01-01"
end
it "returns the next date in the sequence" do
#jan_two.date.should == "2001-01-02"
end
end
end
When I run that test, I get:
working date factories
sequence incrementing
returns a date dimension object ok
returns the next date in the sequence (FAILED - 1)
Failures:
1) working date factories sequence incrementing returns the next date in the sequence
Failure/Error: #jan_two.date.should == "2001-01-02"
expected: "2001-01-02"
got: "2001-01-01" (using ==)
I've read a bunch of other questions related to sequences, but it doesn't seem that I'm making the mistakes identified therein. It's a different (likely dumber) mistake. What is it?
I finally found an approach that works, and is probably a little better anyway. I still don't understand why the code above doesn't work - if someone can explain that to me (maybe with a reference to a doc or part of the source code), I'll go ahead and accept that answer - this post is just for those who follow. Here's what worked:
FactoryGirl.define do
factory :date_dimension do
sequence(:date) { |n| (Date.new(2000,12,31) + n.days).to_s }
calendar_year { Date.parse(date).strftime("%Y") }
day_of_week { Date.parse(date).strftime("%A") }
end
end
The code above passes this test:
describe "working date factories" do
before(:all) do
#date_dimension = FactoryGirl.create(:date_dimension)
#jan_two = FactoryGirl.create(:date_dimension)
end
describe "sequences" do
it "returns the proper first date in the sequence" do
#date_dimension.date.should == "2001-01-01"
#date_dimension.calendar_year.should == "2001"
#date_dimension.day_of_week.should == "Monday"
end
it "returns the next date in the sequence" do
#jan_two.date.should == "2001-01-02"
#jan_two.calendar_year.should == "2001"
#jan_two.day_of_week.should == "Tuesday"
end
end
end
Related
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.
I'm fairly new to Ruby and am currently taking a full stack course. For one of my projects we are building an addressbook. I have set up how to add an entry to the addressbook, however, I can't seem to figure out how to delete an entry (I make an attempt with the remove_entry method in the AddressBook class below but am not having any luck). We are also supposed to test first with RSpec, have the test fail and then write some code to get it to pass. If I didn't include all the info needed for this question let me know (rookie here). Anyway, here is what I have so far:
RSpec
context ".remove_entry" do
it "removes only one entry from the address book" do
book = AddressBook.new
entry = book.add_entry('Ada Lovelace', '010.012.1815', 'augusta.king#lovelace.com')
book.remove_entry(entry)
expect(entry).to eq nil
end
end
AddressBook class
require_relative "entry.rb"
class AddressBook
attr_accessor :entries
def initialize
#entries = []
end
def add_entry(name, phone, email)
index = 0
#entries.each do |entry|
if name < entry.name
break
end
index += 1
end
#entries.insert(index, Entry.new(name, phone, email))
end
def remove_entry(entry)
#entries.delete(entry)
end
end
Entry class
class Entry
attr_accessor :name, :phone_number, :email
def initialize(name, phone_number, email)
#name = name
#phone_number = phone_number
#email = email
end
def to_s
"Name: #{#name}\nPhone Number: #{#phone_number}\nEmail: #{#email}"
end
end
When testing my code with RSpec I receive the following error message:
.....F
Failures:
1) AddressBook.remove_entry removes only one entry from the address book
Failure/Error: expect(entry).to eq nil
expected: nil
got: [#<Entry:0x00000101bc82f0 #name="Ada Lovelace", #phone_number="010.012.1815", #email="augusta.king#lovelace.com">]
(compared using ==)
# ./spec/address_book_spec.rb:49:in `block (3 levels) in <top (required)>'
Finished in 0.02075 seconds (files took 0.14221 seconds to load)
6 examples, 1 failure
Failed examples:
rspec ./spec/address_book_spec.rb:44 # AddressBook.remove_entry removes only one entry from the address book
Just test that the book.entries association is empty:
expect(book.entries).to be_empty
As book is a local variable in your test, you will not get a false negative result if you keep your test atomic. Some best practices on rspec.
Edit:
You can also check the entry was not in the set:
expect(book.entries.index(entry)).to be_nil
or test the change of the array length with:
expect { book.remove_entry(entry) }.to change{book.entries.count}.by(-1)
If you wonder for the be_xxx syntax sugar, if the object respond to xxx?, then you can use be_xxx in your tests (predicate matchers)
I think your expect has an issue. The entry variable is not set to nil, but the entry inside book would be nil.
I think something like this would work better:
expect(book.entries.find { |e| e.name == "Ada Lovelace" }).to eq nil
Better still, your AddressBook could have its own find method, which would make the expect param much nicer, like book.find(:name => "Ada Lovelace").
Finally, I would also put an expect call before the remove_entry call, to make sure its result equals entry.
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.
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!
I've got a date property for a model called 'transaction', when I save it I want to check if it's within a range and if not then set the date to the previous transaction.
It's working but I cannot seem to get my test working, it keeps bombing out when adding the error. Is there a way to stub the 'errors.add' line out so I can isolate my test to check if the tran_date value has changed correctly?
def tran_date_within_financial_year
range = ledger.fin_start..ledger.fin_end
unless range === tran_date
tran = Transaction.all.order("created_at").last
self.tran_date = tran.tran_date
errors.add(:tran_date, "is outside financial year")
end
end
Here is my test
context "date is empty" do
it "should set date to previous transaction" do
#prev_tran = Transaction.make!(:tran_date => Date.new(2012,02,03))
#tran = Transaction.make!(:tran_date => "")
#tran.save
#tran.reload.tran_date.should eq(#prev_tran.tran_date)
end
end
The result when running rspec is:
1) Transaction Saving a transaction date is empty should set date to previous transaction
Failure/Error: #tran = Transaction.make!(:ledger => ledger, :tran_date => "")
ActiveRecord::RecordInvalid:
Validation failed: Tran date is outside financial year
However if I comment out the error.add function then my test passes. So it is setting the value correctly to the previous transaction date. The problem is the errors.add stops the test all together.
Well, the spec simply emphasises that you can't save an invalid object.
And the object is invalid because you add errors to it.
These specs should pass:
context "date is empty" do
it "should set date to previous transaction" do
#prev_tran = Transaction.make!(:tran_date => Date.new(2012,02,03))
#tran = Transaction.make(:tran_date => "")
#tran.should_not be_valid
#tran.errors.should have_key :tran_date
#tran.tran_date.should eq(#prev_tran.tran_date)
end
end