Undefined Method when running rspec test using a stub - ruby-on-rails

I have a model which has a to_csv method and import method, trying to test this in rspec to make sure it does the right thing but having a problem. I get the following error:
Failures:
1) Category Class import should create a new record if id does not exist
Failure/Error: Category.import("filename", product)
NoMethodError:
undefined method `path' for "filename":String
Model:
class Category
...<snip>
def self.import(file, product)
product = Product.find(product)
CSV.foreach(file.path, headers: true, col_sep: ";") do |row|
row = row.to_hash
row["variations"] = row["variations"].split(",").map { |s| s.strip }
category = product.categories.find(row["id"]) || Category.new(row)
if category.new_record?
product.categories << category
else
category.update_attributes(row)
end
end
end
def self.to_csv(product, options = {})
product = Product.find(product)
CSV.generate(col_sep: ";") do |csv|
csv << ['id','title','description','variations']
product.categories.each do |category|
variations = category.variations.join(',')
csv << [category.id, category.title, category.description, variations]
end
end
end
end
My Test:
describe Category do
describe 'Class' do
subject { Category }
it { should respond_to(:import) }
it { should respond_to(:to_csv) }
let(:data) { "id;title;description;variations\r1;a title;;abd" }
describe 'import' do
it "should create a new record if id does not exist" do
product = create(:product)
File.stub(:open).with("filename","rb") { StringIO.new(data) }
Category.import("filename", product)
end
end
end
end

I would just make Category.import take a filename:
Category.import("filename", product)
Then Category.import just passes this filename to the CSV.foreach call:
CSV.foreach(filename, headers: true, col_sep: ";") do |row|
It's then not necessary to stub File.open or any of that jazz.

Related

How to convert an array of objects to CSV in Rails

I have an array of objects. I am trying to create CSV data and allow the user to download that file but I get the following error:
Undefined method 'first_name' for Hash:0x007f946fc76590
employee_csv_data.each do |obj|
csv << attributes.map{ |attr| obj.send(attr) }
end
end
end
This is the button that allows a user to download the CSV:
<%= link_to "Download Employee CSV", download_employee_csv_path %>
Controller:
def download_employee_csv
employee_csv_data = []
employees.each do |employee|
employee_csv_data << {
first_name: employee[:first_name],
last_name: employee[:last_name],
email: employee_email,
phone1: employee[:phone1],
gender: employee[:gender],
veteran: employee[:veteran].to_s,
dob: employee[:dob],
core_score: service_score,
performance_rank: rank,
industry_modules_passed: industry_modules_passed
}
end
respond_to do |format|
format.html
format.csv { send_data Employer.to_csv(employee_csv_data), filename: "download_employee_csv.csv" }
end
end
employee_csv_data:
=> [{:first_name=>"Christopher",
:last_name=>"Pelnar",
:email=>"pelnar#gmail.com",
:phone1=>"4072422433",
:gender=>"male",
:veteran=>"true",
:dob=>"1988-09-09",
:core_score=>"No Score",
:performance_rank=>"No Rank",
:industry_modules_passed=>"No Industry Modules Passed"},
{:first_name=>"chris",
:last_name=>"pelnar",
:email=>"chris#gmail.com",
:phone1=>"4072422433",
:gender=>"male",
:veteran=>"true",
:dob=>"1998-09-09",
:core_score=>"729",
:performance_rank=>"Good",
:industry_modules_passed=>"Entry-Service, Entry-Tech"}]
Model:
def self.to_csv(employee_csv_data)
attributes = %w(first_name last_name email phone gender veteran dob core_score performance_rank industry_modules_passed)
CSV.generate(headers: true) do |csv|
csv << attributes
employee_csv_data.each do |obj|
csv << attributes.map{ |attr| obj.send(attr) }
end
end
end
When I click the button, it takes me to the blank HTML page without any problem. When I add .csv to the filename in the URL on that page I get the error.
It looks like it's an array of Hashes. To access properties of a hash in Ruby you need to use brackets. Try updating your code to this:
csv << attributes.map{ |attr| obj.send([], attr) }
or more concisely:
csv << attributes.map{ |attr| obj[attr] }
One more thing, in the example you provided, the keys in the hash are symbols which means you may need to convert your attributes to symbols when trying to access them, like this:
csv << attributes.map{ |attr| obj[attr.to_sym] }
I adapted #Ctpelnar1988's answer to determine the attributes dynamically and allow each array item to have different columns:
def array_of_hashes_to_csv(array)
array_keys = array.map(&:keys).flatten.uniq
CSV.generate(headers: true) do |csv|
csv << array_keys
array.each do |obj|
csv << array_keys.map{ |attr| obj[attr] }
end
end
end
Example:
puts array_of_hashes_to_csv([
{attr_a: 1, attr_b: 2},
{attr_a: 3, attr_c: 4}
])
attr_a,attr_b,attr_c
1,2,
3,,4
In the more specific "employee_csv_data" context, I think it'd look like this:
def self.to_csv(employee_csv_data)
attributes = employee_csv_data.map(&:keys).flatten.uniq
CSV.generate(headers: true) do |csv|
csv << attributes
employee_csv_data.each do |obj|
csv << attributes.map { |attr| obj[attr] }
end
end
end

Named scope with multiple values

I'm having some trouble with my named scope.
def self.by_status(status)
arr = status.split(',').map{ |s| s }
logger.debug "RESULT: #{arr.inspect}"
where(status: arr)
end
When I call this scope with more than one value, the result of arr = ["New", "Open"]
This does not return any results, while it should. If I try this command in the console: Shipment.where(status: ['New', 'Open']) I get the results that I'm expecting.
Am I missing something here?
Edit (added the call of the class method ):
def self.to_csv(options = {}, vendor_id, status)
CSV.generate(options) do |csv|
csv << column_names
if !vendor_id.blank? && status.blank?
by_vendor_id(vendor_id).each do |product|
csv << product.attributes.values_at(*column_names)
end
elsif !vendor_id.blank? && !status.blank?
by_vendor_id(vendor_id).by_status(status).each do |product|
csv << product.attributes.values_at(*column_names)
end
elsif vendor_id.blank? && !status.blank?
logger.debug "by_status result: #{by_status(status).inspect}"
by_status(status).each do |product|
csv << product.attributes.values_at(*column_names)
end
else
all.each do |product|
csv << product.attributes.values_at(*column_names)
end
end
end
end
Try this in your model:
scope :by_status, ->(*statuses) { where(status: statuses) }
Then in your code you can call:
Shipment.by_status('New', 'Open')
This has the flexibility to just take one argument, too:
Shipment.by_status('New')

Undefined Method in Rails even though the method exist

I have a method in my view helper directory that I am trying to use within a model but i keep getting a undefined method error. I cannot figure out what i am doing wrong.This is my module.
module StbHelper
def gen_csv(stbs)
CSV.generate do |csv|
csv << [
'param1',
'param2'
]
stbs.each do |stb|
health_check = stb.stb_health_checks.last
csv << [
'value1',
'value2'
]
end
end
end
This is the class i want to use the method in.
require 'stb_helper'
class Stb < ActiveRecord::Base
def self.get_notes_data
.
.
.
end
def self.update
.
.
.
end
def self.report(options={})
csv_file = nil
if options == {}
########################################
# This is the line that throws the error
csv_file = StbHelper.gen_csv(Stb.all)
#######################################
else
stbs = []
customers = List.where(id: options[:list])[0].customers
customers.each do |customer|
customer.accounts.each do |account|
stbs += account.stbs
end
end
csv_file = StbHelper.gen_csv(stbs)
end
end
end
You've defined a module, that doesn't require instantiation. You should be able to use it without the StbHelper part (as long as you require the module in the document):
def self.report(options={})
csv_file = nil
if options == {}
########################################
# This is the line that throws the error
csv_file = gen_csv(Stb.all)
#######################################
else
stbs = []
customers = List.where(id: options[:list])[0].customers
customers.each do |customer|
customer.accounts.each do |account|
stbs += account.stbs
end
end
csv_file = gen_csv(stbs)
end
end
But you shouldn't use a helper for this, you can create a normal module and require it the same way.
Edit: Save the module in a new folder called app/modules (and restart the server), save a file called stb_helper.rb with the contents of your module:
module StbHelper
def gen_csv(stbs)
CSV.generate do |csv|
csv << [
'param1',
'param2'
]
stbs.each do |stb|
health_check = stb.stb_health_checks.last
csv << [
'value1',
'value2'
]
end
end
end

Rspec bypass inner function and return mock data

In rails I am writing a test for a controller method search_backups with Rspec:
def elastic_mongo_lookup(search_term)
devices_ids_from_elastic = ConfigTextSearch.search search_term
puts devices_ids_from_elastic
device_ids = devices_ids_from_elastic.map { |device| device._source.device_id }
csv_string = CSV.generate do |csv|
Device.where(:_id.in => device_ids).each do |device|
csv << [device.logical_name, device.primary_ip]
end
end
return csv_string
end
def search_backups
authorize! :read, :custom_report
csv_string = elastic_mongo_lookup params[:search_term]
if csv_string.blank?
flash[:notice] = "No results were found"
redirect_to reports_path
else
render text: "DeviceID, primary_ip\n" + csv_string
end
end#search_backups
describe "try controller method" do
let(:reports_controller) { ReportsController.new }
before do
allow(CSV).to receive(:generate).and_return("1234", "blah")
allow(ConfigTextSearch).to receive(:search).and_return(['"hits": [ {"_source":{"device_id":"54afe167b3000006"}]'])
allow(:devices_ids_from_elastic).to receive(:map).and_return('54afe167b3000006')
stub_request(:get, "http://localhost:9200/mongo_index/config_files/_search?q=").
with(:headers => {'Expect'=>'', 'User-Agent'=>'Faraday v0.9.1'}).
to_return(:status => 200, :body => '', :headers => {})
end
it "allows people to search backups" do
reports = double(ReportsController)
post 'search_backups'
end
end
The issue is that ConfigTextSearch.search search_term returns a elasticsearch ORM object.. which means I can't stub it because the .map() method on devices_ids_from_elastic.map is unique with it's nested _source method.
How could I bypass elastic_mongo_lookup entirely and just return a mocked csv_string to search_backups?
In an RSpec controller test, controller is defined as the controller under test. You can therefore achieve what you're asking about with the following:
allow(controller).to receive(:elastic_mongo_lookup).and_return('whatever string you choose')

rspec test result from csv.read mocking file

I'm using ruby 1.9 and I'm trying to do BDD. My first test 'should read in the csv' works, but the second where I require a file object to be mocked doesn't.
Here is my model spec:
require 'spec_helper'
describe Person do
describe "Importing data" do
let(:person) { Person.new }
let(:data) { "title\tsurname\tfirstname\t\rtitle2\tsurname2\tfirstname2\t\r"}
let(:result) {[["title","surname","firstname"],["title2","surname2","firstname2"]] }
it "should read in the csv" do
CSV.should_receive(:read).
with("filename", :row_sep => "\r", :col_sep => "\t")
person.import("filename")
end
it "should have result" do
filename = mock(File, :exists? => true, :read => data)
person.import(filename).should eq(result)
end
end
end
Here is the code so far:
class Person < ActiveRecord::Base
attr_accessor :import_file
def import(filename)
CSV.read(filename, :row_sep => "\r", :col_sep => "\t")
end
end
I basically want to mock a file so that when the CSV method tries to read from the file it returns my data variable. Then I can test if it equals my result variable.
You can stub File.open:
let(:data) { "title\tsurname\tfirstname\rtitle2\tsurname2\tfirstname2\r" }
let(:result) {[["title","surname","firstname"],["title2","surname2","firstname2"]] }
it "should parse file contents and return a result" do
expect(File).to receive(:open).with("filename","rb") { StringIO.new(data) }
person.import("filename").should eq(result)
end
StringIO essentially wraps a string and makes it behave like an IO object (a File in this case)
Stubbing File.open breaks pry. To avoid this you can stub CSV.read, which isn't as robust as stubbing file, but will let you use pry:
let(:data) do
StringIO.new <<-CSV_DATA
0;48;78;108;279;351;405;694;872;1696
1.03;9.28;13.4;18.56;29.9;30.93;42.27;77.32;85.57;100.0
0.0;2.94;8.82;11.76;44.12;97.06;100.0;100.0;100.0;100.0
CSV_DATA
end
let(:csv_config) { { headers: true, return_headers: true, converters: :numeric } }
before { allow(CSV).to receive(:read).with(csv_path, csv_config) { CSV.parse(data, csv_config) } }

Resources