How do I test complex relationships in Rails with RSpec? - ruby-on-rails

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

Related

Scheduling rails emails/remainders depending on user date input

I have a setup where I would like to send automatic reminders for users if they haven't made a monthly rental payment. The landlord has the opinion to define the day of the month on which the tenant should make the payment(due_date). What I would like to achieve is that if the payment hasn't been made in 2 days since the due_date the tenant would be sent an email. Currently, I have the following task for this.
namespace :rent_due_reminder do
desc "Sends monthly reminders to tenants who haven't paid their rent."
task :send => :environment do
Offer.all.each do |offer|
if offer.status == "accepted" && offer.due_date.present?
landlord = User.find_by(id: offer.landlord_id)
tenant = User.find_by(id: offer.user_id)
stripe_account = Stripe::Account.retrieve(landlord.merchant_id)
payments = Stripe::Charge.list(
{
limit: 100,
expand: ['data.source_transfer.source_transaction.dispute', 'data.application_fee'],
source: {object: 'all'}
},
{ stripe_account: landlord.merchant_id }
)
due_date = "#{Date.today.year}-#{Date.today.month}-#{offer.due_date}".to_datetime
last_payment_date = DateTime.strptime(payments.data.last.created.to_s,'%s')
if (SOME CONDITION)
OfferMailer.with(offer: offer, tenant: tenant, landlord: landlord).rent_due.deliver
end
end
end
end
end
So what I'm doing is that I'm picking the last payment and getting the date of that and this is what I should use to condition the mailer. Initially, I thought that I could just look and see whether last_payment_date < due_date + 2, but this didn't seem to work. I might be getting confused about this since if I have multiple landlords and some of them have their due date for example 30th of every month and some of them have 1st of every month, should I still be running the task monthly or how should I set this up? Apologies for the reasonably bad explanation, I myself am getting very confused with this also.
There is a flaw in your logic. Even if a user made a payment on the due date, the last_payment date would still be less than due_date+2. So you need to add one more check that ensures that the payment was made for the current month.
e.g. If the due date for my rent is the 5th of every month you need to check the payment was made between 1st and 5th. Currently you are only checking if the last payment date was before 7th. That includes valid payments too.
Of course the above logic is flawed too because people could make advance payments and you will need to correct for that too. One way around it is to create a bill and ensure that all bills are paid rather than simply checking for last payments. That way you can calculate delinquencies for more than one month.

Select Model based on year variable

I am building a new app in Rails for an internal project that changes slightly each year based on the requirements of our clients. Any changes between years will occur within the models (add/remove columns, formatting, reports, etc). My plan is to build it to the requirements for this year and going forward each year I will create a new model and migration (e.g. Sample2019Record, Sample2020Record) that will encapsulate the requirements for that year. The app also needs to render previous year data and all the data is scoped based on the year meaning there is no need to render or query multiple years data. I would prefer not to create a new app each year since that is more apps that need to be maintained.
So my idea is to include the year into the URL (/2018/sample/new or /sample/new?year=2018) and parse the model based on the year ("Sample#{year}Record"). Can rails handle this safely and is there a Gem that can help assist with this approach?
Here is what I came up with, thanks for the advice.
routes.rb
get '/:year/samples', to: 'samples#index', as: :samples, defaults: { year: Time.current.year }
Routes will always default to the current year
application_controller.rb
before_action :check_year
def check_year
if params.has_key?(:year)
if "Sample#{params[:year]}Record".safe_constantize.nil?
redirect_to root_path(year: Time.current.year), notice: "Invalid Year"
end
else
redirect_to root_path(year: Time.current.year), notice: "Invalid Year"
end
end
def get_sample_record(year=Time.current.year)
"Sample#{year}Record".safe_constantize
end
Added a before_action to check the year parameter and added the get_sample_record method to safely constantize the record that can be called from any controller with an optional year like so:
sample_controller.rb
sample_2018_record = get_sample_record
sample_2018_record.count
#> 304
sample_2017_record = get_sample_record 2017
sample_2017_record.count
#> 575683
The result will be nil if an invalid year is passed so I will handle the check in the controller.
As #DaveNewton said, this seems like it should work fine so long as you keep data corresponding to different years' requirements in different tables. A few other observations:
Rails has a helper method constantize for parsing a model from a string:
klass_name = "Sample#{year}Record"
record = klass_name.constantize.new()
will make the variable record an instance of your class corresponding to the year variable. You may find it helpful to use a Factory pattern to encapsulate the process.
Also. be careful how you name and organise your files. You may find this thread helpful when working with the Rails infectors for classes with numbers in their names. A big part of working with Rails is allowing its magic to work for you rather than unwittingly trying to work against it.
As a general rather than Rails-specific piece of advice, I'd also give a considerable amount of thought to how you could define a common public interface for records that will persist across years. A codebase featuring things like
if record.instance_of? Sample2018Record
record.my_2018_method
elsif record.instance_of? Sample2019Record
record.my_method_only_relevant_to_2019
...
will become very difficult to reason about, especially for developers who join after a couple of years. Ruby has extremely powerful tools to help you duck type very effectively.

Stub specific instances of a class in a functional test

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])

Rails - Testing a method that uses DateTime.now

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.

Testing Internationalised Dates in Rspec

I have some business logic in a controller that I want to test that involves setting two values to today's date and yesterday's date.
Initially I had a passing test that essentially looked like this;
controller:
def wibble
#start_time = Date.yesterday
end
test:
it 'blah blah'
get :wibble
assigns(:start_date).should eq(Date.yesterday)
end
But a new requirement has been added that means the date should be i18n'd, which means that the controller is returning back something different.
My Thoughts
I had thought about mocking the variables, because I could only care that they are set to something, but then the business logic of Today and Yesterday isn't being exercised.
I also considered forcing i18n on the test, but this seems way to brittle.
Can anyone suggest a good way to test this?

Resources