I have a cronjob, which moves users from one table to another according some deadline reached.
This cronjob works in rails console, but the test is red. If I test the function from this cronjob, the test is green. When I go inside the cronjob with binding.pry, it holds all necessary variables and does its work correctly.
What can be wrong?
Test:
describe 'try various methods' do
before(:each) do
Obparticipant::Participant.all.delete_all
#content = FactoryBot.create(:content, :with_department_ob, target_group: 'child', subject: 'Infos für Teilnehmer aus {ort}', message: '«{geschlecht} | Lieber | Liebe» {vorname}, du bist am {geburtsdatum} geboren.', notification_email: '{nachname}, {straße}, {plz}, {wohnland}, {bundesland}, {landesgruppe}')
germany = ::Physical::Base::Country.GERMAN
address = FactoryBot.create(:address, addressline_1: 'Sesamstraße', addressline_2: 'Kaufmannstraße', state: 'Bayern', city: 'München', zip: '80331', country_id: germany.id)
person = FactoryBot.create(:person, firstname: 'Pablo', lastname: 'Domingo', dateofbirth: Date.new(2001,2,3), gender: 'm', address_id: address.id)
#participant = FactoryBot.create(:participant, person_id: person.id)
#participant.open_todos.by_task(:account_data).each{ |t| t.complete! }
end
it 'should move recipients with a start_date of today back to content_recipients' do
person_two = FactoryBot.create(:person)
participant_two = FactoryBot.create(:participant, person_id: person_two.id, program_season_id: #participant.program_season_id)
participant_two.open_todos.by_task(:account_data).each{ |t| t.complete! }
filter = '{"program_season":"' + #participant.program_season_id.to_s + '"}'
#content.update_attributes(for_dynamic_groups: true, filter: filter, is_draft: false, delay_days: 5)
FactoryBot.create(:delayed_content_recipient, content_id: #content.id, recipient_id: participant_two.id, start_date: Date.today)
expect(#content.content_recipients.size).to eq(0)
Cronjobs.check_recipients # or #content.insert_open_recipients
expect(#content.delayed_content_recipients.size).to eq(1)
expect(#content.content_recipients.map(&:recipient_id).last).to eq(participant_two.id) # this expectation fails, when a cronjob is tested, and passes, when a function is tested
end`
Cronjob:
def self.check_recipients
contents = ::Content.published.current.by_for_dynamic_groups(true)
contents.each do |content|
content.insert_open_recipients
end
end
Function
def insert_open_recipients
search = ::SimpleParticipantSearch.new(JSON.parse(self.filter))
new_recipients = search.result.without_content(self.id)
new_recipients.each do |nr|
if self.delay_days.present?
unless self.delayed_content_recipients.map(&:recipient_id).include?(nr.id)
self.delayed_content_recipients.create(content_id: self.id, recipient_id: nr.id, start_date: Date.today + self.delay_days.days)
end
else
self.participant_recipients << nr unless errors_with_participant?(nr)
end
end
if self.delayed_content_recipients.any?
self.delayed_content_recipients.each do |recipient|
if new_recipients.map(&:id).include?(recipient.recipient_id)
if recipient.start_date == Date.today
self.delayed_content_recipients.delete(recipient)
self.participant_recipients << Obparticipant::Participant.find_by(id: recipient.recipient_id) unless errors_with_participant?(Obparticipant::Participant.find_by(id: recipient.recipient_id))
end
else
self.delayed_content_recipients.delete(recipient)
end
end
end
end
The solution I found is to test separately whether a Cronjob is run, and whether the function it calls works.
I wrote a stub for this Cronjob in the cronjobs controller rspec
it 'should call the correct method on the Cronjobs.check_recipients object' do
Cronjobs.stub(:check_recipients)
post :create, job: 'CheckRecipients'
expect(Cronjobs).to have_received(:check_recipients)
expect(response).to have_http_status(200)
end
and tested the function in the test i provided above.
it 'should move recipients with a start_date of today back to content_recipients' do
person_two = FactoryBot.create(:person)
participant_two = FactoryBot.create(:participant, person_id: person_two.id, program_season_id: #participant.program_season_id)
participant_two.open_todos.by_task(:account_data).each{ |t| t.complete! }
filter = '{"program_season":"' + #participant.program_season_id.to_s + '"}'
#content.update_attributes(for_dynamic_groups: true, filter: filter, is_draft: false, delay_days: 5)
FactoryBot.create(:delayed_content_recipient, content_id: #content.id, recipient_id: participant_two.id, start_date: Date.today)
expect(#content.content_recipients.size).to eq(0)
#content.insert_open_recipients
expect(#content.delayed_content_recipients.size).to eq(1)
expect(#content.content_recipients.map(&:recipient_id).last).to eq(participant_two.id)
end
Related
I have a model Appointment that prohibit the object to be created using a past date or update if the field day is in the past.
class Appointment < ApplicationRecord
belongs_to :user
...
validate :not_past, on: [:create, :update]
private
...
def not_past
if day.past?
errors.add(:day, '...')
end
end
end
But I need to make a test file using RSpec to test if it really cannot be edited if the field day is a past date.
require 'rails_helper'
RSpec.describe Appointment, type: :model do
...
it 'Cannot be edited if the date has past' do
#user = User.last
r = Appointment.new
r.day = (Time.now - 2.days).strftime("%d/%m/%Y")
r.hour = "10:00"
r.description = "Some Description"
r.duration = 1.0
r.user = #user
r.save!
x = Appointment.last
x.description = "Other"
expect(x.save).to be_falsey
end
...
end
The trouble is, the test can't be accurate due to an error that prohibit the creation of an Appointment object with the past day.
What should I do to force, or even maybe make a fake object with a past date for I can finally test it?
You can use update_attribute which will skip validations.
it 'Cannot be edited if the date has past' do
#user = User.last
r = Appointment.new
r.day = (Time.now - 2.days).strftime("%d/%m/%Y")
r.hour = "10:00"
r.description = "Some Description"
r.duration = 1.0
r.user = #user
r.save!
x = Appointment.last
x.description = "Other"
r.update_attribute(:day, (Time.now - 2.days).strftime("%d/%m/%Y"))
expect(x.save).to be_falsey
end
Also you have a lot of noise in your test (data which is not asserted) which you should avoid by e.g. creating a helper function or using factories.
it 'Cannot be edited if the date has past' do
appointment = create_appointment
appointment.update_attribute(:day, (Time.now - 2.days).strftime("%d/%m/%Y"))
appointment.description = 'new'
assert(appointment.valid?).to eq false
end
def create_appointment
Appointment.create!(
day: Time.now.strftime("%d/%m/%Y"),
hour: '10:00',
description: 'description',
duration: 1.0,
user: User.last
)
end
Also you test for falsey which will also match nil values. What you want to do in this case is test for false with eq false.
I am writing some tests for a relatively complicated action that I need to occur when a record is destroyed.
These are my CheckIn functions and a CheckIn has_one :weigh_in:
def weight_loss
prev_ci = CheckIn.joins(:weigh_in)
.where(client_id: self.client_id)
.where('week < ?', self.week)
.where('weigh_ins.current_weight IS NOT NULL')
.order('week desc').first()
return 0 if self.weigh_in.nil? or prev_ci.nil? or prev_ci.weigh_in.nil?
change = prev_ci.weigh_in.current_weight.to_f - self.weigh_in.current_weight.to_f
return change.round(2)
end
def prev_ci
prev_ci = CheckIn.joins(:weigh_in)
.where(client_id: self.client_id)
.where('week < ?', self.week)
.where('weigh_ins.current_weight IS NOT NULL')
.order('week desc').first()
return prev_ci
end
def post_ci
post_ci = CheckIn.joins(:weigh_in)
.where(client_id: self.client_id)
.where('week > ?', self.week)
.where('weigh_ins.current_weight IS NOT NULL')
.order('week desc').first()
return nil if post_ci.nil? or post_ci.weigh_in.nil?
return post_ci
end
In my WeighIn model I have the following callbacks:
class WeighIn < ActiveRecord::Base
belongs_to :check_in
before_save :add_weightloss
after_destroy :adjust_weightloss
def add_weightloss
self.weightloss = 0
self.weightloss = self.check_in.weight_loss
end
def adjust_weightloss
post_ci = self.check_in.post_ci
unless post_ci.nil?
post_ci.weigh_in.weightloss = 0
post_ci.weigh_in.weightloss = post_ci.weight_loss
# Prints the correct value to pass the test (39.7)
p 6, post_ci.weigh_in
post_ci.weigh_in.save!
end
end
end
But my final test (for deletion) still fails:
RSpec.describe WeighIn, type: :model do
it "before_save weigh_in" do
ci1 = CheckIn.create!(client_id: 1, program_id: 1, week: 1)
ci2 = CheckIn.create!(client_id: 1, program_id: 1, week: 2)
ci3 = CheckIn.create!(client_id: 1, program_id: 1, week: 3)
ci4 = CheckIn.create!(client_id: 1, program_id: 1, week: 4)
wi1 = WeighIn.create!(check_in_id: ci1.id, client_id: 1, date: Date.today, current_weight: 100)
wi2 = WeighIn.create!(check_in_id: ci2.id, client_id: 1, date: Date.today, current_weight: 90)
wi3 = WeighIn.create!(check_in_id: ci4.id, client_id: 1, date: Date.today, current_weight: 70.5)
# Verifies functionality of before save
expect(wi1.weightloss).to eq 0
expect(wi2.weightloss).to eq 10
expect(wi3.weightloss).to eq 19.5
# Verifies fucntionality of update
wi_params = { check_in_id: ci4.id, client_id: 1, date: Date.today, current_weight: 60.3 }
wi3.update(wi_params)
expect(wi3.weightloss).to eq 29.7
# Verifies functionality of destroy
wi2.destroy
# Prints incorrect, old value, 29.7
p 'in test', wi3
expect(wi3.weightloss).to eq 39.7
end
end
It seems a though the functions are working properly but the record reverts back or is never saved / updated?
Can you try this to reload the object to check for a change?
expect(wi3.reload.weightloss).to eq 39.7
The main thing I am looking to achieve from this question is understanding. With some assistance I have been looking at refactoring my controller code into more manageable modules/classes so that I can test them effectively. I have an example here that I would like to work on, my question is how would I test the class Sale:
class TransactionsController < ApplicationController
def create
payment = BraintreeTransaction::VerifyPayment.new(params, #user_id, #transaction_total)
payment.run(params)
if payment.success?
redirect_to thank_you_path
else
flash.now[:alert] = payment.error
flash.keep
redirect_to new_transaction_path
end
end
module BraintreeTransaction
class VerifyPayment
def initialize(params, user_id, total)
#transaction_total = total
#user_id = user_id
#params = params
#error_message = nil
end
def run(params)
#result = BraintreeTransaction::Sale.new.braintree_hash(params, #transaction_total)
if #result.success?
#cart_items = CartItem.where(user_id: #user_id).where.not(image_id: nil)
#cart_items.destroy_all
create_real_user
update_completed_transaction
guest_user.destroy
#success = true
else
update_transaction
#error_message = BraintreeErrors::Errors.new.error_message(#result)
end
end
def success?
#success
end
def error
#error_message
end
end
module BraintreeTransaction
class Sale
def braintree_hash(params, total)
Braintree::Transaction.sale(
amount: total,
payment_method_nonce: params[:payment_method_nonce],
device_data: params[:device_data],
customer: {
first_name: params[:first_name],
last_name: params[:last_name],
email: params[:email],
phone: params[:phone]
},
billing: {
first_name: params[:first_name],
last_name: params[:last_name],
company: params[:company],
street_address: params[:street_address],
locality: params[:locality],
region: params[:region],
postal_code: params[:postal_code]
},
shipping: {
first_name: params[:shipping_first_name].presence || params[:first_name].presence,
last_name: params[:shipping_last_name].presence || params[:last_name].presence,
company: params[:shipping_company].presence || params[:company].presence,
street_address: params[:shipping_street_address].presence || params[:street_address].presence,
locality: params[:shipping_locality].presence || params[:locality].presence,
region: params[:shipping_region].presence || params[:region].presence,
postal_code: params[:shipping_postal_code].presence || params[:postal_code].presence
},
options: {
submit_for_settlement: true,
store_in_vault_on_success: true
}
)
end
end
end
I don't know if I am looking at this wrong but this piece of code here BraintreeTransaction::Sale.new.braintree_hash is what I want to test and I want to ensure that when called the class receives a hash ?
Update
So far I have come up with this (though I am not 100% confident it is the correct approach ?)
require 'rails_helper'
RSpec.describe BraintreeTransaction::Sale do
#transaction_total = 100
let(:params) { FactoryGirl.attributes_for(:braintree_transaction, amount: #transaction_total) }
it 'recieves a hash when creating a payment' do
expect_any_instance_of(BraintreeTransaction::Sale).to receive(:braintree_hash).with(params, #transaction_total).and_return(true)
end
end
I get an error returned which I don't understand
Failure/Error: DEFAULT_FAILURE_NOTIFIER = lambda { |failure, _opts| raise failure }
Exactly one instance should have received the following message(s) but didn't: braintree_hash
I might not be spot on but I would answer the way I would have tackled the issue. There are three ways you can write a test that hits the code you want to test.
Write a unit test for braintree_hash for BraintreeTransaction::Sale object
Write a controller unit method for create method in TransactionsController controller
write an integration test for route for create method in TransactionsController.
These are the ways you can start exploring.
A couple of things here. All the suggestions for refactoring your code (from your other question Writing valuable controller tests - Rspec) apply here. I can make further suggestions on this code, if helpful.
In your test, I believe your problem is that you never actually call BraintreeTransaction.new.braintree_hash(params) (which should be called immediately following your expect_any_instance_of declaration). And so no instances ever receive the message(s).
I have a below method in my model and I want to stub the value of below
"esxi_hosts = RezApi.new.get_esxi(type, vrealm_id)"
with values like [{x: "y"}]
what is the way to do it in rspec.
def create_esxi
if (["vRealm", "Praxis Parent vRealm", "Praxis Child vRealm"].include?(self.collection_type.try(:name)))
esxi_hosts = []
if(( self.parent && self.parent.parent && self.parent.collection_type.name.upcase == "POD" && self.parent.parent.collection_type.name.upcase == "DATACENTER") or ( self.parent && self.parent.parent && self.parent.parent.parent && (self.parent.collection_type.name.upcase == "RELEASE - PRAXIS" or self.parent.collection_type.name.upcase == "RELEASE - SUBSCRIPTION") && self.parent.parent.collection_type.name.upcase == "POD" && self.parent.parent.parent.collection_type.name.upcase == "DATACENTER"))
vrealm_type = collection_type.try(:name)
vrealm_id = "d#{self.parent.parent.instance}p#{self.parent.instance}v#{instance}"
case vrealm_type
when "vRealm"
types = ["vrealm-multitenant-dr2c", "vpc-standard"]
types.each do |type|
if esxi_hosts.empty?
esxi_hosts = RezApi.new.get_esxi(type, vrealm_id)
end
end
when "Praxis Parent vRealm"
esxi_hosts = RezApi.new.get_esxi("praxis-core", vrealm_id)
when "Praxis Child vRealm"
esxi_hosts = RezApi.new.get_esxi("praxis-node-mgmt", vrealm_id)
end
end
if esxi_hosts.flatten.any?
assign_esxi(esxi_hosts)
end
end
end
I have already tried the below code but it didnt work
require 'spec_helper'
describe "Esxi Host creation from Rez Api" do
let(:federation) {create(:federation_collection, parent_id: nil, name: "Test", usage: "Tech Ops hosted in Vmware Data Centers", owner: "--", date_from: "2015-08-26", date_to: nil, collection_type_id: 2, zone_name: "se.vpc.vmw")}
let(:datacenter) {create(:datacenter_collection, name: "Datacenter", parent_id: federation.id)}
let(:pod) {create(:pod_collection, parent_id: datacenter.id)}
let(:fqdn) {"d2p1s0ch10srv0v101-esx0.se.vpc.vmw"}
let(:vrealm1) {create(:vrealm_collection, name: "vRealm1", parent_id: pod.id)}
let(:vrealm2) {create(:vrealm_collection, name: "vRealm2", parent_id: pod.id)}
context "When response has esxi hosts " do
let(:rez_response) {[{"esx0"=>{"nodefqdn"=>"d2p1s0ch10srv0.se.vpc.vmw","fqdn"=>"d2p1s0ch10srv0v101-esx0.se.vpc.vmw","vmk0"=>{"pg_name"=>"d2p1v101-esx-pg-1062","ip_addr"=>"10.141.112.71","netmask"=>"255.255.255.0",},"vmk1"=>{"pg_name"=>"d2p1pod-sto-pg-17","ip_addr"=>"172.16.160.51","netmask"=>"255.255.252.0"},"vmk2"=>{"pg_name"=>"d2p1v101-ftx-pg-1063","ip_addr"=>"172.16.165.126","netmask"=>"255.255.255.0"}}}]}
subject {RezApi.new}
it "should create esxi hosts" do
allow(subject).to receive(:get_esxi).with("type", "101").and_return(rez_response)
expect(subject).to receive(:get_esxi).with("type", "101").and_return(rez_response)
vrealm1.create_esxi
vrealm1.resources.map(&:fqdn).should include(fqdn)
end
end
end
Getting the below error
Failures:
1) Esxi Host creation from Rez Api When response has esxi hosts should create esxi hosts
Failure/Error: expect(subject).to receive(:get_esxi).with("type", "101").and_return(rez_response)
(#<RezApi:0xc26d69c>).get_esxi("type", "101")
expected: 1 time with arguments: ("type", "101")
received: 0 times
subject { RezApi.new }
context "....." do
it "....." do
allow(subject).to receive(:get_esxi).with("type", "101").and_return({x: "y"})
expect(subject).to receive(:get_esxi).with("type", "101").and_return({x: "y"})
end
end
So, your updated test code should be this:
describe "Esxi Host creation from Rez Api" do
let(:federation) {create(:federation_collection, parent_id: nil, name: "Test", usage: "Tech Ops hosted in Vmware Data Centers", owner: "--", date_from: "2015-08-26", date_to: nil, collection_type_id: 2, zone_name: "se.vpc.vmw")}
let(:datacenter) {create(:datacenter_collection, name: "Datacenter", parent_id: federation.id)}
let(:pod) {create(:pod_collection, parent_id: datacenter.id)}
let(:fqdn) {"d2p1s0ch10srv0v101-esx0.se.vpc.vmw"}
let(:vrealm1) {create(:vrealm_collection, name: "vRealm1", parent_id: pod.id)}
let(:vrealm2) {create(:vrealm_collection, name: "vRealm2", parent_id: pod.id)}
context "When response has esxi hosts " do
let(:rez_response) {[{"esx0"=>{"nodefqdn"=>"d2p1s0ch10srv0.se.vpc.vmw","fqdn"=>"d2p1s0ch10srv0v101-esx0.se.vpc.vmw","vmk0"=>{"pg_name"=>"d2p1v101-esx-pg-1062","ip_addr"=>"10.141.112.71","netmask"=>"255.255.255.0",},"vmk1"=>{"pg_name"=>"d2p1pod-sto-pg-17","ip_addr"=>"172.16.160.51","netmask"=>"255.255.252.0"},"vmk2"=>{"pg_name"=>"d2p1v101-ftx-pg-1063","ip_addr"=>"172.16.165.126","netmask"=>"255.255.255.0"}}}}
subject { RezApi.new }
it "should create esxi hosts" do
allow(subject).to receive(:get_esxi).with("vrealm-multitenant-dr2c", "101").and_return(rez_response)
expect(subject).to receive(:get_esxi).with("vrealm-multitenant-dr2c", "101").and_return(rez_response)
vrealm1.create_esxi
vrealm1.resources.map(&:fqdn).should include(fqdn)
end
end
end
It worked with below code
RezApi.any_instance.stub(:get_esxi).with("vrealm-multitenant-dr2c", "101").and_return(success_response)
I'm probably misunderstanding something here.
I have a model Secondant that I create with Fabrication in my model rspec.
main_user = Fabricate :user, email: TESTEMAIL
sec = Fabricate :secondant, email: SECEMAIL, user_id: main_user.id
sec_user = Fabricate :user, email: SECEMAIL
ActionMailer::Base.deliveries = []
debugger
At this point when I look at the value of secondant_id in the sec model, the attribute is empty (it get's filled in a after_create callback). When i retrieve the model just created from the database that attribute is filled. Why are those two not in sync?
27 main_user = Fabricate :user, email: TESTEMAIL
28 sec = Fabricate :secondant, email: SECEMAIL, user_id: main_user.id
29 sec_user = Fabricate :user, email: SECEMAIL
30 ActionMailer::Base.deliveries = []
31 debugger
=> 32 sec.destroy
33 end
34
35 it 'should have a secondant_id assigned' do
36 sec.secondant_id.should_not be_nil
(rdb:1) e sec
#<Secondant id: 519, user_id: 1095, email: "secondant#hotmail.com", secondant_id: nil, created_at: "2013-10-10 13:13:29", updated_at: "2013-10-10 13:13:29", reported: false>
(rdb:1) e Secondant.where(id: sec.id).first
#<Secondant id: 519, user_id: 1095, email: "secondant#hotmail.com", secondant_id: 1096, created_at: "2013-10-10 13:13:29", updated_at: "2013-10-10 13:13:29", reported: false>
My after_create callback:
def find_user
user = User.where(email: self.email).first
if user
# create the link to the user
self.secondant_id = user.id
self.save
# see if this is the second one
if Secondant.where('user_id = ? and secondant_id is not null', user_id).count == 2
user.do_somthing
end
end
return
end
EDIT
There is a similar callback in the user class, which is firing in this case (thanks Peter)
def find_secondant
Secondant.where(email: email).find_each do |sec|
sec.secondant_id = id
sec.save
end
end
At the time you create sec, the user with the identical email has not been created, so your after_save callback should not be setting secondant_id.
I can only assume that your find_user method is getting invoked as a result of the User creation or the where operation you are executing in the debugger, resulting in the secondant_id field being set at that time. It won't be reflected in sec unless/until you do a reload, as the Ruby object created by where is distinct from the sec Ruby object.