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
Related
require 'requiredclass'
class Test
def get_client()
return some_client
end
def intermediate_method()
res = nil
self.class
.get_client
.retry(tries:5, on: [RequiredClass::ClientTimeout]) do |myclient|
call_count += 1
res = myclient.dosomething()
end
return res
end
def method_to_test()
x = intermediate_method()
y = false
return x && y
end
end
How can I write rspec for method_to_test here. How can I mock get_client.retry as well as calls to get_client while also mocking res variable assignment so that gets assigned the value i would like it to assign.
As written, this code is difficult to test. That's a smell and a sign that the code should be restructured. Really any time you feel tempted to mock a method in the current class, that's a sign that the thing you want to mock does not belong in that class. It should be injected (passed in) instead. Like this:
require 'requiredclass'
class Test
attr_reader :client
def initialize(client)
#client = client
end
def method_to_test
x = intermediate_method
y = false
x && y
end
def intermediate_method
res = nil
client.retry(tries: 5, on: [RequiredClass:ClientTimeout]) do |my_client|
call_count += 1
res = my_client.do_something
end
res
end
end
Given this refactored code, the tests might look like this:
RSpec.describe Test do
subject(:test) { Test.new(client) }
let(:client) { instance_double(Client, retry: true, do_something: true) }
describe '#method_to_test'
subject(:method_to_test) { test.method_to_test }
it 'returns false' do
expect(method_to_test).to be_false
end
end
end
In this code I've passed a double with a stubbed retry method into the Test class on instantiation. You could optionally use a mock, instead. That would look like this:
RSpec.describe Test do
subject(:test) { Test.new(client) }
let(:client) { instance_double(Client) }
before do
allow(client).to receive(:retry)
allow(client).to receive(:do_something)
end
describe '#method_to_test'
subject(:method_to_test) { test.method_to_test }
it 'returns false' do
expect(method_to_test).to be_false
end
end
end
There's a good write up of mocks and doubles in the RSpec documentation.
I am trying to create a request spec for a form submission and my recaptcha verification is causing the test to fail. I have a pretty simple test:
RSpec.describe "PotentialClients", type: :request do
let(:pc_attributes) { ... }
describe "POST /potential_clients" do
it "should create record" do
expect { post potential_clients_path, params: { potential_client: pc_attributes } }
.to change(PotentialClient, :count).by(+1)
end
end
end
I run into an issue because in PotentialClients#create I make a call to verify_recaptcha? which returns false in the test instead of true:
# potential_clients_controller.rb
def create
#potential_client = PotentialClient.new(potential_client_params)
page_success = verify_recaptcha?(params[:recaptcha_token], 'lead_creation_page')
if page_success && #potential_client.save
...
end
end
# application_controller.rb
def verify_recaptcha?(token, recaptcha_action)
secret_key = ENV['CAPTCHA_SECRET_KEY']
uri = URI.parse("https://www.google.com/recaptcha/api/siteverify?secret=#{secret_key}&response=#{token}")
response = Net::HTTP.get_response(uri)
json = JSON.parse(response.body)
if json['success'] && json['score'] > RECAPTCHA_MINIMUM_SCORE && (json['action'] == "lead_creation_page" || json['action'] == "lead_creation_modal")
return true
elsif json['success'] == false && json["error-codes"].include?("timeout-or-duplicate")
return true
end
return false
end
How should I mock the call to verify_recapthca? so that my test passes? I've tried:
allow(PotentialClient).to receive(:verify_recaptcha?).and_return(true)
# and
allow_any_instance_of(PotentialClient).to receive(:verify_recaptcha?).and_return(true)
but both threw errors:
PotentialClient(...) does not implement: verify_recaptcha?
allow(PotentialClient).to receive(:verify_recaptcha?).and_return(true)
This isn't working because—as the error message says—PotentialClient (the model) doesn't have a method called verify_recaptcha?. The method is defined in ApplicationController, which is extended by PotentialClientsController, and that's where you need to mock it.
My Rails is rusty, but it looks like in an rspec-rails controller spec the current instance of the controller is exposed by the controller method. In that case, what you want is this:
allow_any_instance_of(ApplicationController).to receive(:verify_recaptcha?).and_return(true)
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 want to assign a confirmation code to my users while creating one. And I also titleize some columns before saving-updating them. So my user.rb looks like this (it may be a bit messy):
// user.rb
*** some code ***
before_save { titleize_column(:name)
titleize_column(:surname)
capitalize_column(:complaints)
capitalize_column(:education)
capitalize_column(:job)
capitalize_column(:complaintsdetails)
capitalize_column(:prediagnosis)
capitalize_column(:existingdiagnosis)
capitalize_column(:knownilnessesother)
capitalize_column(:usedmedicine)
capitalize_column(:operation)
capitalize_column(:trauma)
capitalize_column(:allergy)
capitalize_column(:otherhabits)
capitalize_column(:motherother)
capitalize_column(:fatherother)
capitalize_column(:siblingsother)
}
before_save :generate_confirmation_code
protected
def generate_confirmation_code
unless self[:confirmed]
if(self[:type] == 'Patient')
update_attribute :confirmation_code, SecureRandom.urlsafe_base64(20)
update_attribute :confirmed, false
else
update_attribute :confirmed, true
end
end
end
protected
def capitalize_column(attr)
unless self[attr].nil?
self[attr] = Unicode::capitalize self[attr]
end
end
protected
def titleize_column(attr)
unless self[attr].nil?
words = self[attr].split
words.each_with_index do |v,i|
words[i] = Unicode::capitalize v
end
self[attr] = words.join(" ")
end
end
I'm using separate methods for titleizing and capitalizing columns because they may be nil when first creating a user, so I'm checking if it is null or not in those methods. This structure works fine on a normal signup with strong parameters. However, if I try to use twitter signup with the method below, it gives me the error 'stack level too deep' and I can see that it calls the generate_confirmation_code 123 times from the application trace and then these happens:
app/models/user.rb:83:in each'
app/models/user.rb:83:ineach_with_index'
app/models/user.rb:83:in titleize_column'
app/models/user.rb:20:inblock in '
app/models/user.rb:64:in generate_confirmation_code' (x123 times)
app/models/user.rb:101:infrom_omniauth'
app/controllers/socials_controller.rb:4:in `create'
// method for signing up/logging in a user from twitter
class << self
def from_omniauth(auth_hash)
if exists?(uid: auth_hash['uid'])
user = find_by(uid: auth_hash['uid'])
else
user = find_or_create_by(uid: auth_hash['uid'], provider: auth_hash['provider'], type: 'Patient')
user.password_digest = User.digest('111111')
user.name = auth_hash['info']['name']
user.location = get_social_location_for user.provider, auth_hash['info']['location']
user.avatar = auth_hash['info']['image']
user.url = get_social_url_for user.provider, auth_hash['info']['urls']
user.save! // THIS IS THE LINE 101!
conversation = Conversation.create()
user.conversation = conversation
admin = Admin.first
admin.conversations << conversation
user.progress = Progress.create(active_state:1)
end
user
end
I think I'm messing up by using before_save not properly, but do not know how to do it right. What am I doing wrong here?
update_attribute also fires the save callbacks, thereby looping the before_save infinitely, thus producing stack level too deep.
You can just simply assign values in a before_save callback methods, because they will simply be saved afterwards anyway. See the following:
def generate_confirmation_code
unless self[:confirmed]
if(self[:type] == 'Patient')
self.confirmation_code = SecureRandom.urlsafe_base64(20)
self.confirmed = false
else
self.confirmed = true
end
end
end
You are calling update_attribute inside before_save callback method, instead you can just assign values to attributes. The method signature generate_confirmation_code should be like below -
def generate_confirmation_code
unless self[:confirmed]
if(self[:type] == 'Patient')
self.confirmation_code = SecureRandom.urlsafe_base64(20)
self.confirmed = false
else
self.confirmed = true
end
end
end
I have this action in my rails controler,
def step_submit
validate_user()
#owning = #user.create_user_car_transaction(Variant.find(params[:variant]), params[:details], params[:address], params[:somethin1])
Contact.user_contact(current_user, params[:contact]) if #user.contact.nil?
redirect_to "/next_step"
end
I use codeClimate to check the quality of the code..
it shows this action's complexity ~ 30 ..
I actually broke a really huge method into this.. how can i still reduce this complexity?
these are the different methods the action calls
def self.user_contact(user, contact_hash = nil)
contact = user.contact || user.create_contact()
contact.update_attributes(contact_hash) if contact_hash.present?
contact
end
def validate_user
if params[:user] && current_user.nil?
user = User.create(params[:user])
sign_in user
end
end
def create_user_car_transaction(car, details_hash, address_hash, coupon_hash = nil)
transaction = self.transactions.create()
car.transaction_item = transaction.transaction_items.create()
car.save
payment_hash = details_hash
payment_hash.merge!(address_hash)
payment = transaction.create_payment(payment_hash)
transaction.update_attributes(:status=>"1") if transaction.status.nil?
transaction
end