Understanding how to test a class using RSpec - ruby-on-rails

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

Related

Rails Rspec - How to test if Service has been called in another Service

While writing tests, I stopped at trying to test Service in another Service. In such a situation, I should probably just check if Service has been called because it has already been tested elsewhere. I did a little research on the Internet and found something like have_received but I have no idea how to use it in my example.
check_service.rb
Class CheckService
def initialize(params)
#params = params
end
def self.call(params)
new(params).call
end
def call
CheckUser.call(params[:user_id])
end
end
check_service_spec.rb
...
describe 'call' do
let(:result) { CheckService.call(params) }
let(:params) { { user_id: "100" } }
let(:check_user) { instance_double(CheckUser) }
before do
allow(check_user).to receive(:call).and_return(true)
end
it do
result
expect(check_user).to have_received(:call)
end
end
...
I was trying something like this (it's simple example), but I get error:
(InstanceDouble(CheckUser) (anonymous)).call(*(any args))
expected: 1 time with any arguments
received: 0 times with any arguments
Is there any option to test situation I presented?
Short anwser
describe 'call' do
let(:result) { CheckService.call(params) }
let(:params) { { user_id: "100" } }
## let(:check_user) { instance_double(CheckUser) } delete this
before do
allow(CheckUser).to receive(:call).and_return(true)
end
it do
result
expect(CheckUser).to have_received(:call)
end
end
Alternative
I think a better way to test this is to use DI (Dependency Injection), so you pass CheckUser as a dependency to CheckService. I prefer to write the whole test inside the it block too!
class CheckService
def initialize(params, check_handler:)
#params = params
#check_handler = check_handler
end
def self.call(params, check_handler: CheckUser)
new(params, check_handler: check_handler).call
end
def call
#check_handler.call(#params[:user_id])
end
end
describe 'call' do
it 'check user with params' do
check_user = class_double(CheckUser)
allow(check_user).to receive(:call).and_return(true)
params = { user_id: "100" }
CheckService.call(params, check_handler: check_user)
expect(check_user).to have_received(:call)
end
end
A blog post to read more about -> https://blog.testdouble.com/posts/2018-05-17-do-we-need-dependency-injection-in-ruby/

Testing Cronjobs in Rails

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

RSpec - testing race conditions

I'm trying to write a RSpec controller test checking for race conditions based on this blog post, but created Threads don't "see" the Users created at the beginning of the test.
The blog post suggests to change config.use_transactional_fixtures to false, but even with this setting Users aren't visible.
Here's the test:
it "avoids race conditions" do
u = create(:user, :without_settings)
u_2 = create(:user, :without_settings)
wait_for_it = true
threads = Array.new(4).map {
Thread.new do
true while wait_for_it
# send the request
signed_post(
u,
api_v1_blockades_path,
params: {
blockade: {
blocked_user_id: u_2.id,
reason: 11
}
}
)
end
}
wait_for_it = false
threads.map(&:join)
expect(u.blockades.count).to eq(1)
end
For authentication i'm using the ApiAuth gem:
before_action :api_authenticate
private
def current_user
#current_user ||= User.find_by(id: ApiAuth.access_id(request))
end
def api_authenticate
head(:unauthorized) unless current_user && ApiAuth.authentic?(request, current_user.auth_token)
end

Trigger rails controller function - Paypal Website Standard IPN

I've got a Paypal IPN that comes into a PaymentNotificationsController in my app. However, some variables depend on the number of items in a cart, so i want to extract them before creating the PaymentNotification.
So far, i've got:
class PaymentNotificationsController < ApplicationController
protect_from_forgery except: [:create]
def create
PaymentNotification.create!(params: params,
item_number: params[:item_number], item_name: params[:item_name], quantity: params[:quantity]
render nothing: true
end
end
However, when the notification comes from PayPal, it comes in the form of item_name1, item_number1, quantity1, item_name2, item_number2, quantity2 and so on.
Even if its just one item, it would come as item_name1, item_number1, quantity1, option1 and so on.
I have this function to try and extract the variables, but i don't know how to trigger the function. I tried using a before_action at the top of the controller but it didn't work. Returned wrong number of arguments(0 for 1):
ITEM_PARAM_PREFIXES = ["item_name", "item_number", "quantity"]
def extract_ipn_items_params(params)
item_params = []
loop do
item_num_to_test = item_params.length + 1
item_num_suffix = item_num_to_test.to_s
possible_param_name = ITEM_PARAM_PREFIXES[0] + item_num_suffix
if params.include?(possible_param_name)
this_item_params = {}
ITEM_PARAM_PREFIXES.each do |prefix|
this_item_params[prefix] = params[prefix + item_num_suffix]
end
item_params.push this_item_params
else
return item_params
end
end
end
So i'm asking, how do i trigger the function to extract the variables and put them into params[:item_number], params[:item_name], params[:quantity] for each item in the cart so if there are two items, two separate PaymentNotifications would be created?
Note: Both methods are in the same PaymentNotificationsController.
Any help would be appreciated. Thanks in advance!
I assume your method extract_ipn_items_params already fetches the data you require, you can remove the params argument to the method, as the params is always available in the actions/methods of the controller.
ITEM_PARAM_PREFIXES = ["item_name", "item_number", "quantity"]
def extract_ipn_items_params
mod_params = Hash.new{|k, v| k[v] = {} }
ITEM_PARAM_PREFIXES.each do |item_data_key|
key_tracker = 1
loop do
current_key = (item_data_key + key_tracker.to_s).to_sym
if params.include? current_key
mod_params[key_tracker][item_data_key] = params[current_key]
else
break
end
key_tracker += 1
end
end
mod_params
end
The method returns a hash of hashes like:
{1 => {item_name: 'Item 1', item_number: 1084, quantity: 15}}, if you have nested attributes set up for a user, I think you should be able to do something like, not really sure, but should be possible:
user.update(payment_notifications_attributes: extract_ipn_items_params)
Let me know if that works for you.
UPDATE
Based on the Github Gist, here's something I was able to come up with:
class PaymentNotificationsController < ApplicationController
protect_from_forgery except: [:create]
ITEM_PARAM_PREFIXES = ["item_name", "item_number", "quantity", "option_name"]
def create
extract_ipn_items_params.each do |key, values|
# this approach loops through all the returned results, nested attributes may help abstract this though
PaymentNotification.create(values)
render nothing: true
end
def details
# params.extract_ipn_items_params #this doesn't exist as params is an instance of ActionController::Parameters
PaymentNotification.update_attributes(line_item_id: params[:item_number], product_title: params[:item_name], option_name: params[:option_name], quantity: params[:quantity])
end
private
def additional_attributes
# create this for additional merge attributes. A better place for these would be the parent of this
{
params: params,
cart_id: params[:invoice],
status: params[:payment_status],
transaction_id: params[:txn_id],
first_name: params[:first_name],
last_name: params[:last_name],
email: params[:payer_email],
address_name: params[:address_name],
address_street: params[:address_street],
address_city: params[:address_city],
address_state: params[:address_state],
address_zip: params[:address_zip],
address_country: params[:address_country]
}
end
def extract_ipn_items_params
mod_params = Hash.new{|k, v| k[v] = {}.merge(additional_attributes) }
ITEM_PARAM_PREFIXES.each do |item_data_key|
key_tracker = 1
loop do
current_key = (item_data_key + key_tracker.to_s).to_sym
if params.include? current_key
mod_params[key_tracker][item_data_key] = params[current_key]
else
break
end
key_tracker += 1
end
end
mod_params
end
end
Let me know if that fixes your problem.
You should have payment_id so you can find it by using gem 'paypal-sdk-rest'
payment = PayPal::SDK::REST::Payment.find payment_id
then you could see all details in payment object

Create or Update Rails 4 - updates but also creates (Refactoring)

In my Rails API I have the following code in my Child model:
before_create :delete_error_from_values, :check_errors, :update_child_if_exists
def delete_error_from_values
#new_error = self.values["error"]
#values = self.values.tap { |hs| hs.delete("error") }
end
def update_child_if_exists
conditions = {type: self.type, parent_id: self.parent_id}
if existing_child = Child.find_by(conditions)
new_values = existing_child.values.reverse_merge!(#values)
hash = {:values => new_values}
existing_child.update_attributes(hash)
end
end
def check_errors
if self.type == "error"
conditions = {type: self.type, parent_id: self.parent_id}
if existing_child = Child.find_by(conditions)
bd_errors = existing_child.error_summary
bd_errors[#new_error] = bd_errors[#new_error].to_i + 1
hash = {:error_summary => bd_errors}
existing_child.update_attributes(hash)
else
self.error_summary = {#new_error => 1}
end
end
end
This works like expected, except for one small detail: The Child is updated if a record by type and parent_id already exists, but it is also created. How can I refactor this to stop creation?
I've tried to include return false, but if I do this, the update is not successful.
I wish to have something like find_or_create_by, but I'm not sure how to use it for this cases.
May be you can refactor your code in following approach:
def create
#parent = Parent.find(params[:parent_id])
existing_child = Child.where(type: child_params[:type], parent_id:
child_params[:parent_id]).first
if existing_child.present?
existing_child.update_attributes(attribute: value_1)
else
#child = #parent.child.build(child_params)
end
#other saving related code goes here.
end
This is just a basic piece of example.
Try creating separate instance methods to keep the Contrller DRY. :)

Resources