How to test a specific line in a rails model using rspec - ruby-on-rails

I have a model with an initializer in it, which basically creates a user from a user hash.
After it gets the user information, it checks whether the "privileges" key in the hash is an array. If it's not, it turns it into an array.
Now the obvious way of doing this would be crafting an entire user_hash so that it would skip those "create user" lines and then check if it turns the input into an array if necessary. However, I was wondering if there is a more DRY way of doing this?
Here is the user model I'm talking about:
def initialize(opts={})
#first_name = opts[:user_hash][:first]
#last_name = opts[:user_hash][:last]
#user_name = opts[:user_hash][:user_name]
#email = opts[:user_hash][:email]
#user_id = opts[:user_hash][:id]
#privileges = {}
if opts[:privs].present?
if !opts[:privs].kind_of?(Array)
opts[:privs] = [opts[:privs]]
end
end
end

You can pass a double which returns the needed value when the proper key is requested, and itself (or something else) otherwise:
it 'turns privs into an array' do
opts = double(:opts)
allow(opts)to receive(:[]).and_return(opts)
allow(opts)to receive(:[]).with(:privs).and_return('not array')
expect(MyClass.new(opts).privileges).to eq(['not array'])
end
Btw, your code could be simplified using the splat operator:
privs = [*opts[:privs]]
sample behavior:
privs = nil
[*privs]
# => []
privs = ['my', 'array']
[*privs]
# => ["my", "array"]
privs = 'my array'
[*privs]
# => ["my array"]

You can even use the idempotent Kernel#Array
def initialize(opts = {})
#first_name = opts[:user_hash][:first]
#last_name = opts[:user_hash][:last]
#user_name = opts[:user_hash][:user_name]
#email = opts[:user_hash][:email]
#user_id = opts[:user_hash][:id]
#privileges = {}
Array(opts[:privs])
end
I hope that helps

Rather than testing the implementation (value is turned into an array), I would test the desired behavior (takes single privilege or multiple privileges):
describe User do
describe '#initialize' do
it "takes single privilege" do
user = User.new(user_hash: {}, privs: 'foo')
expect(user.privileges).to eq(['foo'])
end
it "takes multiple privileges" do
user = User.new(user_hash: {}, privs: ['foo', 'bar'])
expect(user.privileges).to eq(['foo', 'bar'])
end
end
end

Related

How do I test with Rspec an initialize method that invokes find_or_initialize_by?

How do I write a test to check for the find_or_initialize_by block in this initialize method?
def initialize(document, user)
#document = document
#api = #document.api_version
#template = #document.template
#user = user
#brand = #user.brand
#vars = master_var_hash.extend Hashie::Extensions::DeepFind
template_variables.each do |t|
#document.template_variables.find_or_initialize_by(name: t.name) do |d|
d.name = t.name
d.tag = t.tag
d.box_name = t.box_name
d.designator = t.designator
d.order_index = t.order_index
d.master_id = t.id
d.editable = t.editable
d.editable_title = t.editable_title
d.html_box = t.box.stack.html_box if #api == :v3
d.text = t.name == 'title' ? default_title : user_value(t)
end
end
end
I want to be able to test that the right values have been assigned to the #document's TemplateVariables from the class' TemplateVariables. In my coverage report I can't even hit inside the find_or_initialize_by block.
My test for size doesn't really check what I want to test here:
describe 'template_variables' do
it 'initializes all the new vars per document' do
expect(document.template_variables.size).to eq subject.master_var_hash.size
end
end
How can I write a test to check all those values and cover those lines?
You could use
expect(document).to receive_message_chain(:template_variables, :find_or_initialize_by).exactly(8).times
but it's a mess and you would have to also check if each call got proper parameters.
I would suggest extracting this to a method:
Document#initialize_variables(template_variables)
then you could test it as simply as
expect(document).to receive(:initialize_variables).with(expected_hash)
and then you can cover Document#initialize_variables with specs and test it's behavior in depth.

Stack level too deep on user.save

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

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

Rspec Ruby on Rails Test File System in model

I have a model that has a method that looks through the filesystem starting at a particular location for files that match a particular regex. This is executed in an after_save callback. I'm not sure how to test this using Rspec and FactoryGirl. I'm not sure how to use something like FakeFS with this because the method is in the model, not the test or the controller. I specify the location to start in my FactoryGirl factory, so I could change that to a fake directory created by the test in a set up clause? I could mock the directory? I think there are probably several different ways I could do this, but which makes the most sense?
Thanks!
def ensure_files_up_to_date
files = find_assembly_files
add_files = check_add_assembly_files(files)
errors = add_assembly_files(add_files)
if errors.size > 0 then
return errors
end
update_files = check_update_assembly_files(files)
errors = update_assembly_files(update_files)
if errors.size > 0 then
return errors
else
return []
end
end
def find_assembly_files
start_dir = self.location
files = Hash.new
if ! File.directory? start_dir then
errors.add(:location, "Directory #{start_dir} does not exist on the system.")
abort("Directory #{start_dir} does not exist on the system for #{self.inspect}")
end
Find.find(start_dir) do |path|
filename = File.basename(path).split("/").last
FILE_TYPES.each { |filepart, filehash|
type = filehash["type"]
vendor = filehash["vendor"]
if filename.match(filepart) then
files[type] = Hash.new
files[type]["path"] = path
files[type]["vendor"] = vendor
end
}
end
return files
end
def check_add_assembly_files(files=self.find_assembly_files)
add = Hash.new
files.each do |file_type, file_hash|
# returns an array
file_path = file_hash["path"]
file_vendor = file_hash["vendor"]
filename = File.basename(file_path)
af = AssemblyFile.where(:name => filename)
if af.size == 0 then
add[file_path] = Hash.new
add[file_path]["type"] = file_type
add[file_path]["vendor"] = file_vendor
end
end
if add.size == 0 then
logger.error("check_add_assembly_files did not find any files to add")
return []
end
return add
end
def check_update_assembly_files(files=self.find_assembly_files)
update = Hash.new
files.each do |file_type, file_hash|
file_path = file_hash["path"]
file_vendor = file_hash["vendor"]
# returns an array
filename = File.basename(file_path)
af = AssemblyFile.find_by_name(filename)
if !af.nil? then
if af.location != file_path or af.file_type != file_type then
update[af.id] = Hash.new
update[af.id]['path'] = file_path
update[af.id]['type'] = file_type
update[af.id]['vendor'] = file_vendor
end
end
end
return update
end
def add_assembly_files(files=self.check_add_assembly_files)
if files.size == 0 then
logger.error("add_assembly_files didn't get any results from check_add_assembly_files")
return []
end
asm_file_errors = Array.new
files.each do |file_path, file_hash|
file_type = file_hash["type"]
file_vendor = file_hash["vendor"]
logger.debug "file type is #{file_type} and path is #{file_path}"
logger.debug FileType.find_by_type_name(file_type)
file_type_id = FileType.find_by_type_name(file_type).id
header = file_header(file_path, file_vendor)
if file_vendor == "TBA" then
check = check_tba_header(header, file_type, file_path)
software = header[TBA_SOFTWARE_PROGRAM]
software_version = header[TBA_SOFTWARE_VERSION]
elsif file_vendor == "TBB" then
check = check_tbb_header(header, file_type, file_path)
if file_type == "TBB-ANNOTATION" then
software = header[TBB_SOURCE]
else
software = "Unified"
end
software_version = "UNKNOWN"
end
if check == 0 then
logger.error("skipping file #{file_path} because it contains incorrect values for this filetype")
asm_file_errors.push("#{file_path} cannot be added to assembly because it contains incorrect values for this filetype")
next
end
if file_vendor == "TBA" then
xml = header.to_xml(:root => "assembly-file")
elsif file_vendor == "TBB" then
xml = header.to_xml
else
xml = ''
end
filename = File.basename(file_path)
if filename.match(/~$/) then
logger.error("Skipping a file with a tilda when adding assembly files. filename #{filename}")
next
end
assembly_file = AssemblyFile.new(
:assembly_id => self.id,
:file_type_id => file_type_id,
:name => filename,
:location => file_path,
:file_date => creation_time(file_path),
:software => software,
:software_version => software_version,
:current => 1,
:metadata => xml
)
assembly_file.save! # exclamation point forces it to raise an error if the save fails
end # end files.each
return asm_file_errors
end
Quick answer: you can stub out model methods like any others. Either stub a specific instance of a model, and then stub find or whatever to return that, or stub out any_instance to if you don't want to worry about which model is involved. Something like:
it "does something" do
foo = Foo.create! some_attributes
foo.should_receive(:some_method).and_return(whatever)
Foo.stub(:find).and_return(foo)
end
The real answer is that your code is too complicated to test effectively. Your models should not even know that a filesystem exists. That behavior should be encapsulated in other classes, which you can test independently. Your model's after_save can then just call a single method on that class, and testing whether or not that single method gets called will be a lot easier.
Your methods are also very difficult to test, because they are trying to do too much. All that conditional logic and external dependencies means you'll have to do a whole lot of mocking to get to the various bits you might want to test.
This is a big topic and a good answer is well beyond the scope of this answer. Start with the Wikipedia article on SOLID and read from there for some of the reasoning behind separating concerns into individual classes and using tiny, composed methods. To give you a ballpark idea, a method with more than one branch or more than 10 lines of code is too big; a class that is more than about 100 lines of code is too big.

inject method to retrieve data from hash

I'm having trouble getting the method below in my user model to handle a hash ('auth') I'm getting from LinkedIn for user signin:
def self.deep_get auth, *fields
auth.inject(auth) { |acc, e| acc[e] if acc }
end
I call the 'deep_get' method later in my user model as I create a user using omniauth/linkedin gem. However, it's returning nil values for the provider/uid/headline/email user fields that I know are not nil.
I included first_name and last_name fields as an example because this approach is working (not returning nil values), but (as I realize) bad style/exception handling. Any ideas as to why my deep_get inject method isn't working to retrieve the data in the hash as I'd like it to?
def self.create_from_omniauth(auth)
create! do |user|
# i'd like to retrieve user information from linkedin per the following with my inject method, but i am getting nil values when i should be getting data.
# :provider and :uid are on the same branch level of data. first_name,last_name,email,etc. are on a branch just below called 'info'
user.provider = deep_get(auth, :provider)
user.uid = deep_get(auth, :uid)
user.headline = deep_get(auth, :info, :headline)
user.email = deep_get(auth, :info, :email)
# the below is working but i know pokemon exception handling is not good style.
begin
user.first_name = auth["info"]["first_name"]
rescue
end
begin
user.last_name = auth["info"]["last_name"]
rescue
end
try this
def deep_find(obj,key)
if obj.respond_to?(:key?) && obj.key?(key)
obj[key]
elsif obj.respond_to?(:each)
r = nil
obj.find{ |*a| r=deep_find(a.last,key) }
r
end
end
or try this
class Hash
def deep_fetch(key, default = nil)
default = yield if block_given?
(deep_find(key) or default) or nil
end
def deep_find(key)
if key?(key)
self[key]
else
self.values.inject(nil) do |memo, v|
memo = v.deep_find(key) if v.respond_to?(:deep_find)
memo unless memo.nil?
end
end
end
end

Resources