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
Related
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)}
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.
I'm converting my application over to use factories instead of fixtures with Factory_Girl_Rails. I have the following factory defined:
factory :requirement do
sequence(:reqTitle) {|t| "Test Requirement #{t}"}
ignore do
categoryName " "
categoryAbbr " "
end
reqText "This is a test general requirement for the purpose of, um, testing things"
status "Approved"
factory :reqWithCat do
category
end
factory :reqWithNamedCat do
category {create(:category, catName: categoryName, catAbbr: categoryAbbr)}
end
factory :reqFromUserRequirement do
user_requirement
end
end
Then, in the setup section, I run the following snippet:
(0..5).each do |x|
requirement = create(:reqWithCat)
requirement.ind_requirements {|ir| [create(:ind_requirements)]}
end
(0..5).each do |x|
create(:reqWithNamedCat, categoryName: "User Interface", categoryAbbr: "UI")
end
However, my tests are failing, apparently because records aren't being created (for instance, the index test on the requirements controller tells me that there 0 records returned when there should be 10). I run the tests in debug mode, and discover that every requirement created has the exact same id value. Also, each record has the same sequence value.
I believe the duplicate records are failing to save, which is why I'm getting a 0 return. However, I can't see what I've set up incorrectly. What am I missing here?
Once I fixed the user factories to create the related many properly, I then discovered that I had written this factory incorrectly based on the way I was defining scopes in my models. Once that was fixed, the "broken" test began working.
I'm trying to decide how to test a method that simply calculates an average of values on associated records. I'm concerned about testing the implementation vs the actual result returned.
Say I have the following models...
class User
has_many :interviews
def interview_grade
interviews.average(:score).round unless interviews.empty?
end
end
class Interview
belongs_to :user
end
And in user_spec.rb I have...
describe "interview_grade" do
let(:user) {User.new}
context "when the user has interviews" do
before { user.stub_chain(:interviews, :empty?){false} }
it "should return an average of the appraisal ratings" do
user.interviews.should_receive(:average).with(:score).and_return(3.2)
user.work_history_grade.should == 3
end
end
context "when the user has no interviews" do
before {Interview.destroy_all}
it "should return nil" do
user.interview_grade.should be_nil
end
end
end
These tests pass but it feels fragile to me. What if interview_grade should actually calculate the sum of the scores (for example). As I'm just testing that a particular chain of methods is called, this passing test wouldn't tell me that the result is actually incorrect.
I have tried stubbing user.interviews in order to setup the available scores for the test to work with but this seems tricky to do in Rails 3 due to the way associations are lazy loaded. i.e. I can't just create an array of Interview objects because it doesn't respond to the average method.
Any advice greatly appreciated.
Coming back to this 3 years later. I would would approach it entirely differently.
The benefit of the code below is that in order to write tests for InterviewGrader I would no longer need to worry about how the scores are attained.
I just give it the scores and test it gives me the correct output.
Also I would never need to worry about the underlying implementation of InterviewGrader. However, if the logic was changed at a later date, the tests would fail.
The new scores method on User would need to be tested separately.
class InterviewGrader
def self.run scores
new(scores).run
end
attr_reader :scores
def initialize(scores)
#scores = scores
end
def run
scores.inject { |sum, score|
sum + score
}.to_f / number_of_scores
end
private
def number_of_scores
scores.length
end
end
class User
has_many :interviews
def scores
interviews.map(&:score)
end
def interview_grade
InterviewGrader.run(scores)
end
end
class Interview
belongs_to :user
end
This is incorrect usage of stubbing and mocking.
In this case you should only test, that interview_grade works, when average returns nil (and this is only case interviews.empty? is used).
The average method is tested by rails itself. round method by ruby tests (i guess). So you not need to test this methods. This is a general idea to test only your own code.
And if you want to test, how interview_grade is calculated, you should create test data (with fixtures or factories). Because you should test separate (in some case) part of system, and in this case separation is wrong: interviews.average and interviews.empty? are dependent in your code, but in spec they independent.
def interview_grade
interviews.average(:score).try(:round)
end
If you rewrite your method in this way, you no need in stubbing and mocking
I have a class which performs several database operations, and I want to write a unit test which verifies that these operations are all performed within a transaction. What's a nice clean way to do that?
Here's some sample code illustrating the class I'm testing:
class StructureUpdater
def initialize(structure)
#structure = structure
end
def update_structure
SeAccount.transaction do
delete_existing_statistics
delete_existing_structure
add_campaigns
# ... etc
end
end
private
def delete_existing_statistics
# ...
end
def delete_existing_structure
# ...
end
def add_campaigns
# ...
end
end
Rspec lets you assert that data has changed in the scope of a particular block.
it "should delete existing statistics" do
lambda do
#structure_updater.update_structure
end.should change(SeAccount, :count).by(3)
end
...or some such depending on what your schema looks like, etc. Not sure what exactly is going on in delete_existing_statistics so modify the change clause accordingly.
EDIT: Didn't understand the question at first, my apologies. You could try asserting the following to make sure these calls occur in a given order (again, using RSpec):
EDIT: You can't assert an expectation against a transaction in a test that has expectations for calls within that transaction. The closest I could come up with off the cuff was:
describe StructureUpdater do
before(:each) do
#structure_updater = StructureUpdater.new(Structure.new)
end
it "should update the model within a Transaction" do
SeAccount.should_receive(:transaction)
#structure_updater.update_structure
end
it "should do these other things" do
#structure_updater.should_receive(:delete_existing_statistics).ordered
#structure_updater.should_receive(:delete_existing_structure).ordered
#structure_updater.should_receive(:add_campaigns).ordered
#structure_updater.update_structure
end
end
ONE MORE TRY: Another minor hack would be to force one of the later method calls in the transaction block to raise, and assert that nothing has changed in the DB. For instance, assuming Statistic is a model, and delete_existing_statistics would change the count of Statistic in the DB, you could know that call occurred in a transaction if an exception thrown later in the transaction rolled back that change. Something like:
it "should happen in a transaction" do
#structure_updater.stub!(:add_campaigns).and_raise
lambda {#structure_updater.update_structure}.should_not change(Statistic, :count)
end