The purpose of this code is to send an email to a user with an array of products whose discount percentages have reached a given threshold. The products are returned by:
user.notifications
which returns an array of 2 element arrays with the following format:
[[product, notification]]
A notification is an object composed of a discount percentage and a product_id.
send_notification?
checks to see if a user has been sent a notification in the last 7 days and returns a boolean (true if they have not received an email in the last week and false if they have for the product being passed in.)
I have the following job and accompanying test:
class ProductNotificationEmailJob
include SuckerPunch::Job
def perform(user)
user_notifications = user.notifications || []
products = []
notifications = []
user_notifications.each do |notification|
if notification[1].send_notification?
products << notification[0]
notifications << notification[1]
end
end
NotificationMailer.notification_email(user, products).deliver
notifications.each do |notification|
notification.update(notification_date: Time.now)
end
end
end
test:
require 'rails_helper'
describe ProductNotificationEmailJob do
it 'performs' do
notification = ObjectCreation.create_notification
expect(notification.notification_date).to be_nil
user = notification.user
stub = double("Object")
expect(NotificationMailer).to receive(:notification_email).with(user, [notification.my_product.product]).and_return(stub)
expect(stub).to receive(:deliver)
ProductNotificationEmailJob.new.perform(user)
expect(MyProductsNotification.last.notification_date).to_not be_nil
end
end
When I take out the line:
include SuckerPunch::Job
the test passes fine but I cannot get it to pass with that line in though. For some reason with the include SuckerPunch::Job line it seems as though the object creation method does not work and returns nil for all values. I apologize in advance if I didn't give enough detail but I didn't want to post too much code. Leave a comment and I will include any details requested. Thank you for your time I really appreciate it!
After looking at the problem with fresh eyes I realized I was violating encapsulation rules by even trying to do all of that stuff in the ProductNotificationEmailJob class. I extracted into another class and everything runs fine and is perfectly testable.
Related
I have the following module in a Rails api:
module SpeedCalculator
def self.adjustment
Random.rand(0..0.3)
end
def self.calculate_adjustment(track, car_speed)
case track.surface_type
when "asphalt"
car_speed - (car_speed * self.adjustment).ceil
when "gravel"
car_speed - (car_speed * self.adjustment).ceil
when "snow"
car_speed - (car_speed * self.adjustment).ceil
else
car_speed
end
end
end
I can successfully test that the adjustment method works like this:
require 'rails_helper'
RSpec.describe SpeedCalculator do
include SpeedCalculator
it "uses an adjustment value between 0 and 0.3" do
expect(SpeedCalculator.adjustment).to be >= 0
expect(SpeedCalculator.adjustment).to be <= 0.3
end
end
It is possible to make an API request like this:
localhost:3000/api/v1/cars/porsche-911?track=monaco
where you are asking the system to calculate the speed for the given car on the given track.
So I need to write a request spec that for a given car and track, the correct value is returned. But how can I do that when the calculate_adjustment always applies a random number?
I believe that I need to create a mock/stub for self.adjustment, so the test would be something like this:
it "calculates max_speed_on_track when a valid track name is provided" do
Car.create!(name:'Subaru Impreza', max_speed:'120', speed_unit:'km/h')
Track.create(name:'Monaco')
# here create a dummy version of adjustment
# that always returns a fixed value, rather than a random value
# and somehow use that dummy for the request?
# Since x below needs to be a known value for the test to work.
get api_v1_car_path(id: 'porsche-911', track: 'monaco')
expect(response).to have_http_status(200)
expect(response.body).to include_json(car: {max_speed_on_track: x})
end
how can I do that when the calculate_adjustment always applies a random number?
I believe that I need to create a mock/stub for self.adjustment
Exactly! The last thing you want in tests is random behaviour (and random failures). You want reproducible results. So yeah, mock your RNG.
The simplest thing to do would be this:
expect(Random).to receive(:rand).with(0..0.3).and_return(0.1234)
# or better
expect(SpeedCalculator).to receive(:adjustment).and_return(0.1234)
# then proceed with your test
get api_v1_car_path(id: 'porsche-911', track: 'monaco')
...
A further improvement here is to not use controller specs to test business logic. Encapsulate your business logic in an object and test that object (where you can use Dependency Injection technique to full extent). And your controller would simply become something like this:
class CarsController
def show
calculator = MaxSpeedCalculator.new(params[:id], params[:track])
render json: calculator.call
end
end
I'm writing a pretty straightforward method. Whenever a referer has referred 5 people to become new users, I want them to get a refund. This means that when a new user is created, there's a method check_referer that checks to see if the person who referred them (if this person exists) should get a refund because they've now referred 5 people in total.
In the test logs, based on the puts statement, I can tell that the code is working and the refund_membership_fees_paid method is indeed being called once. But the test keeps failing with:
Failure/Error: #referer.should_receive(:refund_membership_fees_paid).exactly(1).times
(#<User:0x007fbf46bf1c58>).refund_membership_fees_paid(any args)
expected: 1 time with any arguments
received: 0 times with any arguments
Test code:
describe User, "Test refund_membership_fees_paid method is called" do
before do
#referer = User.new()
#referer.save(validate:false)
RefererRefundAlert.stub_chain(:new, :async, :perform)
end
it "at 5 users" do
5.times do |index|
u = User.new(referred_by: #referer.id)
u.save(validate:false)
end
#referer.should_receive(:refund_membership_fees_paid).exactly(1).times
end
end
Model code:
def check_referer
if self.referred_by.present? && User.where(referred_by: self.referred_by).count == 5
User.find(self.referred_by).refund_membership_fees_paid
end
end
def refund_membership_fees_paid
puts "refund_membership_fees_paid method"
RefererRefundAlert.new.async.perform(self.id)
end
User.find does not return the same object as #referer; it will return a different instance of User that represents the same user in the database.
Instead of checking whether refund_membership_fees_paid is called, you can verify that the correct user ID is getting passed intoRefererRefundAlert.new.async.perform.
Also, as others mentioned, you should set your expectation before running the tested methods.
RefererRefundAlert.new.async.should_receive(:perform)
.with(#referer.id).exactly(1).times
5.times do |index|
u = User.new(referred_by: #referer.id)
u.save(validate:false)
end
The use of should_receive is to set the expectation for the following action.
For example, if your account.close action is supposed to log the closure, the test would be...
logger.should_receive(:account_closed)
account.close
So your example should be restructured to put the test first...
#referer.should_receive(:refund_membership_fees_paid).exactly(1).times
5.times {User.new(referred_by: #referer.id).save(validate: false)}
I received some excellent help on my last post (Undefined Method in rspec testing) but I was just looking for a bit more help.
I have an rspec integration spec that I basically need to alter code for to get the desired outcome. I cannot alter the spec as it's part of the exercise.
let(:user) { User.new(voucher) }
context 'no voucher' do
let(:voucher) { nil }
it 'should bill default price all the time' do
user.bill
expect(user.orders[0].billed_for).to eql 6.95
... ...
end
end
context 'vouchers' do
describe 'default vouchers' do
let(:voucher) { Voucher.create(:default, credit: 15) }
it 'should not bill user if has a remaining credit' do
user.bill
expect(user.orders[0].billed_for).to eql 0.0
... ...
end
end
I placed some dots just to cut out the unnecessary code.
I pretty much understand what's happen here.
A new user class is being create and set to :user in let. A voucher is then initiliased and passed in depending on the context. no voucher is set for the first test. One is set for the second.
Here's where my questions begin
require 'order'
require 'voucher'
class User
attr_accessor :voucher, :orders
def initialize(orders = [], voucher = nil)
#voucher = voucher
#orders = [orders]
end
def bill
new_order = Order.new(self)
#orders << new_order
end
end
The method is initliased. It has optional parameters. I'm a little unclear on how the initialisation works though as I'm unable to access these set variables at all anywhere.
I'm a little unsure about the scope limitations though as I'm hoping to access some of the vouchers variables from the order class which currently looks like this
class Order
DEFAULT_PRICE = 6.95
attr_accessor :user
def initialize(user)
#user = user
end
def billed_for
price = DEFAULT_PRICE
user.orders.each do |order|
price - order.billed_for
end
price
end
end
shoudl accessing the users voucher class be as easy as user.voucher. ??
Also a smaller question. I'm currently using a factory method so the voucher class can initialise itself.
def self.create(type, *attrs)
the *attrs parameter is essentially an array. I can loop through this and bind it to some expected variables by checking for their presence. ie if array has certain key set this key's value to a variable. Is this the best way or is there another popular way?
I know this is a lot to ask but I'm finding myself really confused and would be grateful if annyone could clear any of this up me. Thanks.
For the User class you can access the voucher and orders variables as they are instance variables and publicly accessible due to the attr_accessor at the top.
For your factory problem I recommend FactoryGirl from Thoughbot it makes building Factory objects very easy. You can also use Rails Fixtures which are static data you create.
Turns out I was initializing the variables in the wrong order. This meant that I was looking for items in the wrong class.
Embarrassing noob mistake! All the better for it :)
I am experiencing something weird when trying to test a model through rspec. I have the code
it "has might that varies by atmost 30" do
instance.init(100)
expect(instance.members.count).to eq(1)
instance.members.each do |member|
puts "#{member.unit.name}"
end
expect(instance.members.count).to eq(1)
end
Whenenver I run the above test, both expect methods pass, signaling that there is only a single "member" record associated with the instance, but when I print out the name of each member, it prints a name twice, saying that there is two different members?
EDIT: Also, the method works correctly on the development server, but only have this issue during tests?
EDIT: method in question
def init(might)
transaction do
self.group.delegations.each do |delegation|
unit = delegation.unit
amount = (might / unit.might) * delegation.fraction
amount = amount.round
unless amount < 1
self.members.create(unit: unit, amount: amount)
end
end
end
end
Consider the following class and methods: (This class is obviously much more complete, but for the sake of this thread...):
class Order < ActiveRecord::Base
def check
if (self.user.phone == "55555555") do
self.a_certain_method
return
end
end
def a_certain_method
# Real implementation goes here
end
end
And the following Unit Test:
describe :do_route do
it "should call a_certain_method if user phone number matches 55555555" do
# Create a user
user = Factory(:user)
# Set hard-coded phone number
user.phone = "55555555"
user.save!
# Create an order made by the ordering user
order = Factory(:order, :ordering_user => user)
# Set expectation for a "a_certain_method" call
mock(order).a_certain_method
# Call the tested method
order.check
end
end
From some reason, the above test produces an RR::Errors::TimesCalledError error, which claims that a_certain_method was called 0 times instead of 1... I've been searching around the web for a solution with no luck.
I've tried building a similiar test on a non-activerecord class, and the test produces no errors.
I've used the debugger to check that it does reach the self.a_certain_method line, and also tried using the following instead of mock(order).a_certain_method:
any_instance_of(Order) do |o|
mock(o).a_certain_method
end
Does anyone have any idea how to solve this issue since i'm kind of desperate...
I figured out what the problem was, it failed since the number was already in the database. so it failed to save the hard coded user.phone change.
Thanks for the help though :)