Rspec Ruby on Rails Test File System in model - ruby-on-rails

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.

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.

How to call a class method in \lib from model in rails?

I'm trying to generate stats for a character created by a form. The user inputs the name, race, class, alignment, and whether or not the stats will be generated randomly, or prioritized (values being assigned from highest to lowest). The form works flawlessly, as I can see the output in a view.
What I am now trying to do is call a method from a class in /lib in the model that will generate the stats; however, I keep getting the following error (I can't post pictures):
NoMethodError in CharactersController#create
undefined method `[]' for nil:NilClass
Extracted source (around line #14):
12 before_save do
13 generate_stats
14 self.strength = #character_stats[:strength]
15 self.dexterity = #character_stats[:dexterity]
16 self.constitution = #character_stats[:constitution]
17 self.intelligence = #character_stats[:intelligence]
Here is a copy of some of my code:
In controllers\characters_controller.rb
class CharactersController < ApplicationController
def create
#character = Character.new(character_info_params)
#character.name = params[:character][:name].capitalize
#character.alignment = "#{params[:character][:alignment_lr]} #{params[:character][:alignment_ud]}"
if #character.save
redirect_to #character
else
render 'new'
end
end
private
def character_info_params
params.require(:character).permit(:name, :race, :class_, :alignment)
end
end
In models\character.rb
class Character < ActiveRecord::Base
require 'random_stats_generator'
attr_accessor :rand_stat_gen
def generate_stats
if #rand_stat_gen == true
#character_stats_inst = RandomStatGenerator.new
#character_stats = #character_stats_inst.generate
end
end
before_save do
generate_stats
self.strength = #character_stats[:strength]
self.dexterity = #character_stats[:dexterity]
self.constitution = #character_stats[:constitution]
self.intelligence = #character_stats[:intelligence]
self.wisdom = #character_stats[:wisdom]
self.charisma = #character_stats[:charisma]
end
#validation passed this point
end
In initializers\stat_builders.rb
require "./lib/random_stat_generator.rb"
In lib/random_stat_generator.rb
class RandomStatGenerator
def initialize
#strength = :strength
#dexterity = :dexterity
#constitution = :constitution
#intelligence = :intelligence
#wisdom = :wisdom
#charisma = :charisma
#character_stats = HashWithIndifferentAccess.new()
end
def self.generate
roll_stats
end
def roll(stat)
#roll_value_1 = (1 + (rand(6)))
#roll_value_2 = (1 + (rand(6)))
#roll_value_3 = (1 + (rand(6)))
#roll_value_4 = (1 + (rand(6)))
#roll_array = [#roll_value_1,#roll_value_2,#roll_value_3,#roll_value_4]
#roll_array = #roll_array.sort_by {|x| x }
#roll_array = #roll_array.reverse
stat = #roll_array[0] + #roll_array[1] + #roll_array[2]
end
def roll_stats
#strength = roll(#strength)
#dexterity = roll(#dexterity)
#constitution = roll(#constitution)
#intelligence = roll(#intelligence)
#wisdom = roll(#wisdom)
#charisma = roll(#charisma)
#character_stats[:strength] = #strength
#character_stats[:dexterity] = #dexterity
#character_stats[:constitution] = #constitution
#character_stats[:intelligence] = #intelligence
#character_stats[:wisdom] = #wisdom
#character_stats[:charisma] = #charisma
return #character_stats
end
end
To me, it looks like the method isn't returning anything, or isn't being called at all.
I've tried a lot of solutions that I've come across online, none of them working. There may be some things that don't really make sense that are left over from these solutions. I'm only just starting with rails, so I'm still trying to get used to everything.
Thanks a lot for your help.
Ruby has really powerful functions for manipulating both hashes and arrays.
Typing out duplicate assignments like:
self.strength = #character_stats[:strength]
self.dexterity = #character_stats[:dexterity]
self.constitution = #character_stats[:constitution]
Is pretty dull. So instead we can simply rewrite the methods to pass hashes around.
class RandomStatGenerator
# This is just a constant containing all the stats we want to generate.
STATS = [:strength, :dexterity, :constitution, :intelligence, :wisdom, :charisma]
# Create a hash with random roll values for each stat
def self.roll_stats
# This is kind of scary looking but actually just creates an
# hash from an array of keys
Hash[STATS.map {|k| [k, self.roll ] } ]
end
private
def self.roll
# Create an array with 4 elements (nil)
ary = Array.new(4)
# We then replace the nil value with a random value 1-6
ary = ary.map do
(1 + (rand(6)))
end
# sort it and drop the lowest roll. return the sum of all rolls.
ary.sort.drop(1).sum
# a ruby ninja writes it like this
Array.new(4).map { 1 + rand(6) }.sort.drop(1).sum
end
end
Output:
irb(main):032:0> RandomStatGenerator.roll_stats
=> {:strength=>14, :dexterity=>14, :constitution=>14, :intelligence=>13, :wisdom=>10, :charisma=>9}
But if you don't intend to actually create instances of a class, than you should use a module instead.
Rails models can either be created with a hash or you can replace its values with a hash:
Character.new(RandomStatGenerator.roll_stats)
#character.assign_attributes(RandomStatGenerator.roll_stats)
So we can use this in Character#generate_stats:
def generate_stats
assign_attributes(RandomStatGenerator.roll_stats)
end
You should use ActiveModel callbacks with extreme prejudice. It is often quite a challenge to regulate where in your application and when in the model lifetime. Since before_save runs after validations means that any validations like validates_presence_of :constitution will fail.
In your case it might be better to simply do it in the controller or use:
before_validation :generate_stats, if: -> { new_record? && #rand_stat_gen }
I would like to suggest the following organisation fo your library
# Use a module at top level
module RandomStatGenerator
STATS = [:strength, :dexterity, :constitution, :intelligence, :wisdom, :charisma]
# Use a class Stats if you need to but I don't see why...
class Stats
def initialize
RandomStatGenerator::STATS.each do |stat|
# Below line will do #stat = :stat
instance_variable_set("##{stat.to_s}", stat)
#character_stats = HashWithIndifferentAccess.new()
end
def roll_stats
#character_stats = RandomStatGenerator.roll_stats
end
end
module_function
# below lines will be considered as module functions
# => call RandomStatGenerator.function_name
def roll
roll_value_1 = (1 + (rand(6)))
roll_value_2 = (1 + (rand(6)))
roll_value_3 = (1 + (rand(6)))
roll_value_4 = (1 + (rand(6)))
roll_array = [roll_value_1,roll_value_2,roll_value_3,roll_value_4]
roll_array = roll_array.sort_by {|x| x }
roll_array = roll_array.reverse
roll_array[0] + roll_array[1] + roll_array[2]
end
def roll_stats
character_stats = {}
STATS.each do |stat|
character_stats[stat] = RandomStatGenerator.roll
end
return character_stats
end
end
Then in your character.rb
def generate_stats
#character_stats = RandomStatGenerator.roll_stats
end

AWS::S3::Errors::NoSuchKey: No Such Key error

I'm trying to create a method that deletes files on an S3 instance, but I am getting a AWS::S3::Errors::NoSuchKey: No Such Key error when I try to call .head or .read on an object.
app/models/file_item.rb
def thumbnail
{
exists: thumbnailable?,
small: "http://#{bucket}.s3.amazonaws.com/images/#{id}/small_thumb.png",
large: "http://#{bucket}.s3.amazonaws.com/images/#{id}/large_thumb.png"
}
end
lib/adapters/amazons3/accessor.rb
module Adapters
module AmazonS3
class Accessor
S3_BUCKET = AWS::S3.new.buckets[ENV['AMAZON_BUCKET']]
...
def self.delete_file(thumbnail)
prefix_pattern = %r{http://[MY-S3-HOST]-[a-z]+.s3.amazonaws.com/}
small_path = thumbnail[:small].sub(prefix_pattern, '')
large_path = thumbnail[:large].sub(prefix_pattern, '')
small = S3_BUCKET.objects[small_path]
large = S3_BUCKET.objects[large_path]
binding.pry
S3_BUCKET.objects.delete([small, large])
end
end
end
end
example url1
"http://projectname-staging.s3.amazonaws.com/images/994/small_thumb.png"
example url2
"http://projectname-production.s3.amazonaws.com/images/994/large_thumb.png"
assuming awssdk v1 for ruby.
small = S3_BUCKET.objects[small_path]
does not actually get any objects.
from: https://docs.aws.amazon.com/AWSRubySDK/latest/AWS/S3/Bucket.html
bucket.objects['key'] #=> makes no request, returns an S3Object
bucket.objects.each do |obj|
puts obj.key
end
so you would need to alter your code to something like:
to_delete = []
S3_BUCKET.objects[small_path].each do |obj|
to_delete << obj.key
end
S3_BUCKET.objects[large_path].each do |obj|
to_delete << obj.key
end
S3_BUCKET.objects.delete(to_delete)
just banged out the code, so the idea is there, you might need to correct/polish it a bit
I was able to come of with a kind of different solution thanks to your answer of #Mircea above.
def self.delete_file(thumbnail)
folder = thumbnail[:small].match(/(\d+)(?!.*\d)/)
to_delete = []
S3_BUCKET.objects.with_prefix("images/#{folder}").each do |thumb|
to_delete << thumb.key
end
# binding.pry
S3_BUCKET.objects.delete(to_delete)
end

How to test a specific line in a rails model using rspec

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

Spec for File creation and writing into File

I am new here. I am working on a project with some tests. I have some problems with writing spec for a class. I am done with some simple specs but I have no clue how to write for this one. Any help will be highly appreciated.
My class
Class Writer
def initialize(filepath)
#filepath = RAILS_ROOT + filepath
#xml_document = Nokogiri::XML::Document.new
end
def open
File.open(#filepath,"w") do |f|
#gz = Zlib::GzipWriter.new(f)
#gz.write(%[<?xml version="1.0" encoding="UTF-8"?>\n])
#gz.write(%[<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n])
yield self
#gz.write(%[</urlset>])
#gz.close
end
end
def write_entry_to_xml(entry)
node = Nokogiri::XML::Node.new( "url" , #xml_document )
node["loc"] = entry.loc
node["changefreq"] = entry.changfreq
node["priority"] = entry.priority
node["lastmod"] = entry.lastmod
#gz.write(node.to_xml)
end
end
What I have written so far is as follows
describe "writer" do
before :each do
#time = Time.now
#filepath = RAILS_ROOT + "/public/sitemap/test/sitemap_test.xml.gz"
File.open(#filepath,"w") do |f|
#gz = Zlib::GzipWriter.new(f)
end
#xml_document = Nokogiri::XML::Document.new
#entry = Sitemap::Entry.new("location", "monthly", "0.8", #time)
end
describe "open" do
it "should create a file and write xml entries to it" do
end
end
describe "write_entry_to_xml" do
it "should format and entry to xml node and write it" do
node = Nokogiri::XML::Node.new( "url" , #xml_document )
node["loc"].should == #entry.loc
node["changefreq"].should == #entry.changfreq
node["priority"].should == #entry.priority
node["lastmod"].shoul == #entry.lastmod
end
end
Can anyone help me in writing the complete specs for this class.
Thanks in advance
I don't have time to do all this for you, but here are examples of how I am testing my code:
actual code
it's spec
Notice this: Ropet::Config.expects(:new).returns(config), this can be used for your Nokogiri::XML::Node#new.
My specs use RSpec and Mocha, I like the simplicity of this setup and what can be done with those simple tools.
Edit: rough spec for
def write_entry_to_xml(entry)
node = Nokogiri::XML::Node.new( "url" , #xml_document )
node["loc"] = entry.loc
node["changefreq"] = entry.changfreq
node["priority"] = entry.priority
node["lastmod"] = entry.lastmod
#gz.write(node.to_xml)
end
It could be something like this, though i don't know the purpose of your code.
it 'writes entry to xml' do
content = double('output')
node = double('node'); node.should_receive(:to_xml).and_return(content);
gz = double('gz'); gz.should_receive(:write).with(content)
w = Writer.new("some_path"); w.open
w.instance_variable_set(:#gz, gz) # i'm guessing #gz is assigned after open only?
entry = # i don't know what entry is
Nokogiri::XML::Node.stub(:new).and_return(node)
node.should_receive(:[]).with("loc", entry.loc)
node.should_receive(:[]).with("changefreq", entry.changefreq)
node.should_receive(:[]).with("priority", entry.priority)
node.should_receive(:[]).with("lastmod", entry.lastmod)
w.write_entry_to_xml(entry)
end

Resources