I want to test that a specific icon gets displayed in the view for a User with a streak more than X no of days. So I need to stub a streak method of the User model.But I want that it stubs the method only for a specific user based on its uid. The test code is given below.
test "should display an icon for users with streak longer than 7 days" do
node = node(:one)
User.any_instance.stubs(:streak).returns([8,10])
get :show,
author: node.author.username,
date: node.created_at.strftime("%m-%d-%Y"),
id: node.title.parameterize
assert_select ".fa-fire", 1
end
The return value is an array, the first value in the array is the no of days in the streak and the second value is the no of posts in that streak.
The line User.any_instance.stubs(:streak).returns([8,10]) stubs any instance of the User class. How can I stub it so that it stubs only those instances where :uid => 1?
Sounds like you should be stubbing the specific instance, rather than the class itself.
User.where.not(uid: 1).each do |user|
user.stubs(:streak).returns([8,10])
end
Or maybe (I can't say for sure without more context), you could optimise this by just doing:
node.author.stubs(:streak).returns([8,10])
Related
For context, I have a controller method called delete_cars. Inside of the method, I call destroy_all on an ActiveRecord::Collection of Cars. Below the destroy_all, I call another method, get_car_nums_not_deleted_from_portal, which looks like the following:
def get_car_nums_not_deleted_from_portal(cars_to_be_deleted)
reloaded_cars = cars_to_be_deleted.reload
car_nums = reloaded_cars.car_numbers
if reloaded_cars.any?
puts "Something went wrong. The following cars were not deleted from the portal: #{car_nums.join(', ')}"
end
car_nums
end
Here, I check to see if any cars were not deleted during the destroy_all transaction. If there are any, I just add a puts message. I also return the ActiveRecord::Collection whether there are any records or not, so the code to follow can handle it.
The goal with one of my feature tests is to mimic a user trying to delete three selected cars, but one fails to be deleted. When this scenario occurs, I display a specific notice on the page stating:
'Some selected cars have been successfully deleted from the portal, however, some have not. The '\
"following cars have not been deleted from the portal:\n\n#{some_car_numbers_go_here}"
How can I force just one record to fail when my code executes the destroy_all, WITHOUT adding extra code to my Car model (in the form of a before_destroy or something similar)? I've tried using a spy, but the issue is, when it's created, it's not a real record in the DB, so my query:
cars_to_be_deleted = Car.where(id: params[:car_ids].split(',').collect { |id| id.to_i })
doesn't include it.
For even more context, here's the test code:
context 'when at least one car is not deleted, but the rest are' do
it "should display a message stating 'Some selected cars have been successfully...' and list out the cars that were not deleted" do
expect(Car.count).to eq(100)
visit bulk_edit_cars_path
select(#location.name.upcase, from: 'Location')
select(#track.name.upcase, from: 'Track')
click_button("Search".upcase)
find_field("cars_to_edit[#{Car.first.id}]").click
find_field("cars_to_edit[#{Car.second.id}]").click
find_field("cars_to_edit[#{Car.third.id}]").click
click_button('Delete cars')
cars_to_be_deleted = Car.where(id: Car.first(3).map(&:id)).ids
click_button('Yes')
expect(page).to have_text(
'Some selected cars have been successfully deleted from the portal, however, some have not. The '\
"following cars have not been deleted from the portal:\n\n#{#first_three_cars_car_numbers[0]}".upcase
)
expect(Car.count).to eq(98)
expect(Car.where(id: cars_to_be_deleted).length).to eq(1)
end
end
Any help with this would be greatly appreciated! It's becoming quite frustrating lol.
One way to "mock" not deleting a record for a test could be to use the block version of .to receive to return a falsy value.
The argument for the block is the instance of the record that would be :destroyed.
Since we have this instance, we can check for an arbitrary record to be "not destroyed" and have the block return nil, which would indicate a "failure" from the :destroy method.
In this example, we check for the record of the first Car record in the database and return nil if it is.
If it is not the first record, we use the :delete method, as to not cause an infinite loop in the test (the test would keep calling the mock :destroy).
allow_any_instance_of(Car).to receive(:destroy) { |car|
# use car.delete to prevent infinite loop with the mocked :destroy method
if car.id != Car.first.id
car.delete
end
# this will return `nil`, which means failure from the :destroy method
}
You could create a method that accepts a list of records and decide which one you want to :destroy for more accurate testing!
I am sure there are other ways to work around this, but this is the best we have found so far :)
If there is a specific reason why the deletion might fail you can simulate that case.
Say you have a RaceResult record that must always refer to a valid Car and you have a DB constraint enforcing this (in Postgres: ON DELETE RESTRICT). You could write a test that creates the RaceResult records for some of your Car records:
it 'Cars prevented from deletion are reported` do
...
do_not_delete_cars = Car.where(id: Car.first(3).map(&:id)).ids
do_not_delete_cars.each { |car| RaceResult.create(car: car, ...) }
click_button('Yes')
expect(page).to have_text(...
end
Another option would be to use some knowledge of how your controller interacts with the model:
allow(Car).to receive(:destroy_list_of_cars).with(1,2,3).and_return(false) # or whatever your method would return
This would not actually run the destroy_list_of_cars method, so all the records would still be there in the DB. Then you can expect error messages for each of your selected records.
Or since destroy_all calls each record's destroy method, you could mock that method:
allow_any_instance_of('Car').to receive(:destroy).and_return(false) # simulates a callback halting things
allow_any_instance_of makes tests brittle however.
Finally, you could consider just not anticipating problems before they exist (maybe you don't even need the bulk delete page to be this helpful?). If your users see a more generic error, is there a page they could filter to verify for themselves what might still be there? (there's a lot of factors to consider here, it depends on the importance of the feature to the business and what sort of things could go wrong if the data is inconsistent).
I have an application with a series of tests (FirstTest, SecondTest etc.)
Each test has a calculation set out its relevant model that gets calculated before being saved to the database set out like this:
#first_test.rb
before_save :calculate_total
private
def calculate_total
...
end
I then have an index page for each user (welcome/index) which displays the current user's results for each test. This all works fine, however I want to work out various other things such as each users average score overall etc.
Is it possible to access the current user from the welcome model?
Currently my welcome.rb is accessing the data follows:
#welcome.rb
def self.total
FirstTest.last.total
end
This obviously access the last overall test NOT the last test from the current user.
I feel like I may have just laid the whole application out in a fairly unintelligent manner...
Thanks in advance x
Well you need to save user_id in a column for each record in FirstTest. Then you can find the total for current user
FirstTest.where(:user_id => current_user.id).last.total
Firstly, this question may stray into opinion but I think it's a valuable question to ask. I will give a very specific example for my application which handles absence management and tracking.
An Account has many Users and a User has many Absences. The Account can create PublicHolidays which should be ignored when calculating the number of days that an Absence uses.
Example: If a person takes a week off, the days used will be 5. If one of those days is a PublicHoliday, the days used would be 4.
I want to implement a method such that when a PublicHoliday is created, the days used for any Absences created prior to the date of creation and which cross the date of the PublicHoliday are recalculated.
My current RSpec test looks like this:
it 'triggers a recalculation of absence days on create for absences created before the date of creation of the public holiday' do
robin = FactoryGirl.create(:robin)
absence = FactoryGirl.create(:basic_absence, user: robin)
expect(absence.days_used).to eq(1)
ph = FactoryGirl.create(:public_holiday, country: "England", account: robin.account)
expect(absence.reload.days_used).to eq(0)
end
In this test, ph is the same date as the absence so I expect it to calculate one day to start with and then I intend to use an after create callback to recalculate the days used.
Is this the right way to do this test? Is there a more efficient way without creating a number of associated objects?
Firstly - it's good practice to use lets instead of local variables, and secondly - split your tests so each test tests just one thing. Thirdly: anything that sets up a context for tests should be put into a context-block (even if there's only one test in that context)
eg, here's a re-writing of your spec the standard way:
let(:robin) { FactoryGirl.create(:robin) }
let(:absence) { FactoryGirl.create(:basic_absence, user: robin) }
context "with no public holidays" do
it 'counts the absence day to be a day used' do
expect(absence.days_used).to eq(1)
end
end
context "with a public holiday for the absence" do
before do
FactoryGirl.create(:public_holiday, country: "England", account: robin.account)
end
it 'does not consider the absence day to be a day used' do
expect(absence.days_used).to eq(0)
end
end
I have a method that uses DateTime.now to perform a search on some data, I want to test the method with various dates but I don't know how to stub DateTime.now nor can I get it working with Timecop ( if it even works like that ).
With time cop I tried
it 'has the correct amount if falls in the previous month' do
t = "25 May".to_datetime
Timecop.travel(t)
puts DateTime.now
expect(#employee.monthly_sales).to eq 150
end
when I run the spec I can see that puts DateTime.now gives 2015-05-25T01:00:00+01:00 but having the same puts DateTime.now within the method I'm testing outputs 2015-07-24T08:57:53+01:00 (todays date).
How can I accomplish this?
------------------update---------------------------------------------------
I was setting up the records (#employee, etc.) in a before(:all) block which seems to have caused the problem. It only works when the setup is done after the Timecop do block. Why is this the case?
TL;DR: The problem was that DateTime.now was called in Employee before Timecop.freeze was called in the specs.
Timecop mocks the constructor of Time, Date and DateTime. Any instance created between freeze and return (or inside a freeze block) will be mocked.
Any instance created before freeze or after return won't be affected because Timecop doesn't mess with existing objects.
From the README (my emphasis):
A gem providing "time travel" and "time freezing" capabilities, making it dead simple to test time-dependent code. It provides a unified method to mock Time.now, Date.today, and DateTime.now in a single call.
So it is essential to call Timecop.freeze before you create the Time object you want to mock. If you freeze in an RSpec before block, this will be run before subject is evaluated. However, if you have a before block where you set up your subject (#employee in your case), and have another before block in a nested describe, then your subject is already set up, having called DateTime.new before you froze time.
What happens if you add the following to your Employee
class Employee
def now
DateTime.now
end
end
Then you run the following spec:
describe '#now' do
let(:employee) { #employee }
it 'has the correct amount if falls in the previous month', focus: true do
t = "25 May".to_datetime
Timecop.freeze(t) do
expect(DateTime.now).to eq t
expect(employee.now).to eq t
expect(employee.now.class).to be DateTime
expect(employee.now.class.object_id).to be DateTime.object_id
end
end
end
Instead of using a freeze block, you can also freeze and return in rspec before and after hooks:
describe Employee do
let(:frozen_time) { "25 May".to_datetime }
before { Timecop.freeze(frozen_time) }
after { Timecop.return }
subject { FactoryGirl.create :employee }
it 'has the correct amount if falls in the previous month' do
# spec here
end
end
Off-topic, but maybe have a look at http://betterspecs.org/
Timecop should be able to handle what you want. Try to freeze the time before running your test instead of just traveling, then unfreeze when you finish. Like this:
before do
t = "25 May".to_datetime
Timecop.freeze(t)
end
after do
Timecop.return
end
it 'has the correct amount if falls in the previous month' do
puts DateTime.now
expect(#employee.monthly_sales).to eq 150
end
From Timecop's readme:
freeze is used to statically mock the concept of now. As your program executes, Time.now will not change unless you make subsequent calls into the Timecop API. travel, on the other hand, computes an offset between what we currently think Time.now is (recall that we support nested traveling) and the time passed in. It uses this offset to simulate the passage of time.
So you want to freeze the time at a certain place, rather than just travel to that time. Since time will pass with a travel as it normally would, but from a different starting point.
If this still does not work, you can put your method call in a block with Timecop to ensure that it is freezing the time inside the block like:
t = "25 May".to_datetime
Timecop.travel(t) do # Or use freeze here, depending on what you need
puts DateTime.now
expect(#employee.monthly_sales).to eq 150
end
I ran into several problems with Timecop and other magic stuff that messes with Date, Time and DateTime classes and their methods. I found that it is better to just use dependency injection instead:
Employee code
class Employee
def monthly_sales(for_date = nil)
for_date ||= DateTime.now
# now calculate sales for 'for_date', instead of current month
end
end
Spec
it 'has the correct amount if falls in the previous month' do
t = "25 May".to_datetime
expect(#employee.monthly_sales(t)).to eq 150
end
We, people of the Ruby world, find great pleasure in using some magic tricks, which people who are using less expressive programming languages are unable to utilize. But this is the case where magic is too dark and should really be avoided. Just use generally accepted best practice approach of dependency injection instead.
I have a Model PromoCode which has a .generate! method, that calls .generate which generates a String using SecureRandom.hex(5) and saves it to the database:
class PromoCode < ActiveRecord::Base
class << self
def generate
SecureRandom.hex 5
end
def generate!
return create! code: generate
end
end
end
Now I want to write a spec that test the uniqueness of the generated string. The .generate method should be called as long as a non existent PromoCode has been generated.
I'm not sure how to do this since I can't really stub out the .generate method to return fixed values (because then it would be stuck in an infinite loop).
This is the passing spec for the model so far:
describe PromoCode do
describe ".generate" do
it "should return a string with a length of 10" do
code = PromoCode.generate
code.should be_a String
code.length.should eql 10
end
end
describe ".generate!" do
it "generates and returns a promocode" do
expect {
#promo = PromoCode.generate!
}.to change { PromoCode.count }.from(0).to(1)
#promo.code.should_not be_nil
#promo.code.length.should eql 10
end
it "generates a uniq promocode" do
end
end
end
Any directions appreciated.
Rspec's and_return method allows you to specify multiple return values that will be cycled through
For example you could write
PromoCode.stub(:generate).and_return('badcode1', 'badcode2', 'goodcode')
Which will cause the first call to generate to return 'badcode1', the second 'badcode2' etc... You can then check that the returned promocode was created with the correct code.
If you want to be race condition proof you'll want a database uniqueness constraint, so your code might actually want to be
def generate!
create!(...)
rescue ActiveRecord::RecordNotUnique
retry
end
In your spec you would stub the create! method to raise the first time but return a value the second time
And what about something like: create a PromoCode, save the result, and try to create a new PromoCode with the code of the previous PromoCode object:
it "should reject duplicate promocode" do
#promo = PromoCode.generate!
duplicate_promo = PromoCode.new(:code => #promo.code)
duplicate_promo.should_not be_valid
end
Also, this is model level, I am assuming you have a key in the database that will save you from race conditions...
If you saves the promocode in database you would have added validations there in the model for uniq promocode. So you can test the same in rspec too.
Like this,
it { should validate_uniqueness_of(:promocode) }
This answer is based on your comment:
I need to make sure that generate! generates a code - no matter what,
until a unique code has been generated.
I feel like you might have a hard time testing this correctly. Unit testing "no matter what" situations with indefinite loops can be a bit of a tricky subject.
I'm not sure how to do this since I can't really stub out the .generate method to return fixed values (because then it would be stuck in an infinite loop).
One possibility to consider might be if instead of doing either one or the other, you tried both? (That is, find a way make it return a fixed number under certain circumstances, and eventually trigger the actual random number. An instance variable counter might help; set it to a random number, count it down, when it's greater than zero return the fixed number, or something along those lines). This still doesn't feel like a perfect test, though, or even a very good one for that matter.
It might be worth looking more into means of generating similar strings with high probability of them being unique, and having some mathematical proof of it being that way. I'm not saying this is the most practical idea either, but if you really need to prove (as you're trying to do with tests), it might be the more probably solution.