RSpec testing model method - ruby-on-rails

I have this method in my models/images.rb model. I am starting with testing and having a hard time coming up with tests for it. Would appreciate your help.
def self.tags
t = "db/data.csv"
#arr = []
csvdata = CSV.read(t)
csvdata.shift
csvdata.each do |row|
row.each_with_index do |l, i|
unless l.nil?
#arr << l
end
end
end
#arr
end

First off a word of advice - CSV is probably the worst imaginable data format and is best avoided unless absolutely unavoidable - like if the client insists that manipulating data in MS Excel is a good idea (it is not).
If you have to use CSV don't use a method name like .tags which can confused for a regular ActiveRecord relation.
Testing methods that read from the file system can be quite difficult.
To start with you might want to alter the signature of the method so that you can pass a file path.
def self.tags(file = "db/data.csv")
# ...
end
That way you can pass a fixture file so that you can test it deterministically.
RSpec.describe Image do
describe "tags" do
let(:file) { Rails.root.join('spec', 'support', 'fixtures', 'tags.csv') }
it 'returns an array' do
expect(Image.tags(file)).to eq [ { foo: 'bar' }, { foo: 'baz' } ]
end
end
end
However your method is very ideosyncratic -
def self.tags
t = "db/data.csv"
#arr = []
self.tags makes it a class method yet you are declaring #arr as an instance variable.
Additionally Ruby's enumerable module provides so many methods for manipulating arrays that using an outer variable in a loop is not needed.
def self.tags(file = "db/data.csv")
csv_data = CSV.read(file)
csv_data.shift
csv_data.compact # removes nil elements
end

Related

Ruby replace if block with guard

I've got a service object which creates the CSV file from assigned data. The call method is pretty simple:
def initialize(data)
#data = data
end
def call
CSV.generate(headers: true, col_sep: ';') do |csv|
csv << csv_headers
data.uniq.each do |contract|
next if contract.transient
payment_details = [
next_payment_date(contract),
I18n.t("contracts.interval_options.#{contract.recurring_transaction_interval&.name}"),
]
csv << payment_details
end
end
end
private
def next_payment_date(contract)
if contract.upcoming_installment.nil?
I18n.t('tables.headers.no_next_payment_date')
else
contract.upcoming_installment.transaction_date.to_s
end
end
It works well but I don't think next_payment_date is really fancy if block, I'm wondering is it possible to replace it with some guard instead?
Because of rubocop I cannot use:
contract.upcoming_installment.nil? ? I18n.t('tables.headers.no_next_payment_date') : contract.upcoming_installment.transaction_date.to_s
In my opinion, the method looks fine as it is. Having better readability is a profitable trade-off for increasing LOCs or introducing methods. That said, there are ways where you can make it an one liner if you really fancy.
contract.upcoming_installment&.transaction_date || I18n.t('tables.headers.no_next_payment_date')
Ruby's safe navigation &. will return nil if upcoming_installment is nil which should fallback to the I18n.

How to refactor nested loops in a Ruby method that exports to CSV?

I have to export some information to CSV. I wrote this code and I don't really like it. I don't know how I can refactor it and get rid of the nested loops.
My relations are the following: Order has many Moves, Move has many Stops.
I have to export all of this to CSV, so I will have multiple lines for the same order.
Here is my (low quality) code:
def to_csv
CSV.generate(headers: true) do |csv|
csv << h.t(self.first.exported_attributes.values.flatten) # headers
self.each do |order|
order.moves.map do |move|
move.stops.map do |stop|
order_data = order.exported_attributes[:order].map do |attributes|
order.public_send(attributes)
end
move_data = order.exported_attributes[:move].map do |attributes|
move.decorate.public_send(attributes)
end
stop_data = order.exported_attributes[:stop].map do |attributes|
stop.decorate.public_send(attributes)
end
csv << order_data + move_data + stop_data
end
end
end
end
end
I did this yesterday:
def to_csv
CSV.generate(headers: true) do |csv|
csv << h.t(self.first.exported_attributes.values.flatten) # headers
self.each do |order|
order.moves.each do |move|
move.stops.each do |stop|
csv << order.exported_attributes[:order].map { |attr| order.public_send(attr) } +
order.exported_attributes[:move].map { |attr| move.decorate.send(attr) } +
order.exported_attributes[:stop].map { |attr| stop.decorate.send(attr) }
end
end
end
end
end
The biggest smell I smell isn't the nested loops, but the near-duplication of how the values are gotten from each model.
Let's extract that duplication into similar methods with the same name, exported_values, on Order, Move and Stop:
class Order
def exported_values
exported_attributes[:order].map { |attrs| { public_send(attrs) }
end
end
class Move
def exported_values
order.exported_attributes[:stop].map { |attrs| { decorate.public_send(attrs) }
end
end
class Stop
def exported_values
move.order.exported_attributes[:move].map { |attrs| { decorate.public_send(attrs) }
end
end
and use them in to_csv:
def to_csv
CSV.generate(headers: true) do |csv|
csv << h.t(first.exported_attributes.values.flatten) # headers
each do |order|
order_values = order.exported_values
order.moves.each do |move|
order_and_move_values = order_values + move.exported_values
move.stops.each do |stop|
csv << order_and_move_values + stop.exported_values
end
end
end
end
end
The above has some additional minor improvements:
Get and concatenate the exported values in the outermost possible loops for efficiency.
Loop over moves and stops with each rather than with map, since the loops are done for side effects rather than return values.
Remove unnecessary uses of self..
Now to_csv isn't so bad. But it still has a little feature envy (that is, it calls too many methods on other objects), so let's extract more methods onto the models:
def to_csv
CSV.generate(headers: true) do |csv|
csv << h.t(first.exported_attributes.values.flatten) # headers
each { |order| order.append_to_csv(csv) }
end
end
class Order
def append_to_csv(csv)
values = exported_values
moves.each { |move| move.append_to_csv(csv, values) }
end
end
class Move
def append_to_csv(csv, prefix)
values = exported_values
stops.each { |stop| stop.append_to_csv(csv, prefix + values) }
end
end
class Stop
def append_to_csv(csv, prefix)
csv << prefix + exported_values
end
end
No more nested loops. The extracted methods are a bit duplicative, but I think if the duplication were extracted they would be unclear.
Next we could try to refactor the exported_values methods into a single method.
Perhaps Order#exported_attributes could be broken up into a method on each class that takes no arguments and returns only that class's exported attributes.
The other difference between the methods is that Order doesn't need .decorator but the other classes do. If it has a decorator, just use that instead of the actual order; if not, just give it a fake one:
class Order
def decorator
self
end
end
You could then define a single exported_values method in an module and include it in all three classes:
def exported_values
exported_attributes.map { |attrs| { decorator.public_send(attrs) }
end
There is one more possible improvement: if it was OK for each model's exported values to remain the same for the lifetime of an instance, you could cache them like this
def exported_values
#exported_values ||= exported_attributes.map { |attrs| { decorator.public_send(attrs) }
end
and inline the values locals in the append_to_csv methods and get the "prefixes" from parent objects in those methods instead of passing them as parameters.
Possibly all of the new methods should be extracted to the decorators rather than to the models; I'm not sure whether your decorators are for CSV generation or only for other purposes.

using print inside def having yield statement

I am trying to print inside a function.
The function is used for invoking a block.
But I don't see the print happening in the function definition.
Please shed a light on this. Basically I am not clear with the control flow.
def find_all
matching_items = []
self.each do |item|
if yield(item)
puts "after yield" #print not happening
matching_items << item
end
end
matching_items
end
p ['a', 'b','c','c'].find_all { |item|
if item == 'a'
true
end
}
If your code is exactly as written, you are defining and independent method find_all defined on main. When you type [1,2,3,4].find_all, you are calling the find_all method on Array, which is defined in the Enumerable method. So you are not calling your method at all.
What you are probably trying to do is
class Array
def find_all
...
end
end
This way, [1,2,3,4].find_all will call this method.
However, note that this is probably a bad idea: you're overriding a core method that in a class that isn't yours, so that could have consequences in other code that you are not able to anticipate if any other code uses the find_all method.
What you might try instead is to define a method that takes the array in as an argument. You might move this to a module, but for now:
def find_all(array)
matching_items = []
array.each do |item|
if yield(item)
puts "after yield" #print not happening
matching_items << item
end
end
matching_items
end
Of course, this is basically what Enumerable#find_all already does, but less efficiently: so perhaps this is just an academic exercise, in which case, great!, but otherwise, why not just use the existing method?
If you are trying to re-open the class Array then, this is how you can do it
class Array
def find_all(matching_items = [])
self.each do |item|
if yield(item)
puts "after yield" #print not happening
matching_items << item
end
end
matching_items
end
end
p ['a', 'b', 'c', 'c'].find_all { |item|
if item == 'a'
true
end
}
Output
after yield
["a"]

Is there a more performant way of creating this array?

for one of my views I want to include a search field with jqueries typeahead function.
The array should contain all the attribute values of a client.
The array for the query is generated the following way:
#clients = []
Client.each do |client|
#clients << client.attributes.values.join(' ')
end
Is this approach performant enough for a dataset of about 3000 entries?
Or is there a better and faster solution?
Thanks in advance.
Cheers,
Patrick
Update
One user mentioned to implement it like this:
#clients = Client.map do |client|
client.attributes.values.join(' ')
end
This is another way to do it. But a benchmark reveals that this is no improvement in performance.
This leaves me with the question: Maybe there is a more performant way, but speaking about a maxium of 3000 records - does it really matter?
You could use .map:
#clients = Client.map do |client|
client.attributes.values.join(' ')
end
Even if ActiveRecord models would implement the map method (which they don't i believe), the two solutions suggested by the OP and #xdazz are time- and memory-complexity-wise equivalent. This can be observed with this simple benchmark:
require 'fruity'
# Dummy client class
class Client < Struct.new(:first_name, :last_name, :position, :company)
class << self
include Enumerable
def each(&block)
5000.times do
yield Client.new('Firstname', 'Lastname', 'CEO', 'Company Inc.')
end
end
end
alias_method :attributes, :to_h
end
compare do
schnika do
clients = []
Client.each do |client|
clients << client.attributes.values.join(' ')
end
nil
end
xdazz do
clients = Client.map do |client|
client.attributes.values.join(' ')
end
nil
end
end
Which will output
schnika is similar to xdazz
Also, when you look at the implementation of map (synonymous to collect), it becomes clear that really nothing else happens than in the OP's method:
static VALUE
rb_ary_collect(VALUE ary)
{
long i;
VALUE collect;
RETURN_ENUMERATOR(ary, 0, 0);
collect = rb_ary_new2(RARRAY_LEN(ary));
for (i = 0; i < RARRAY_LEN(ary); i++) {
rb_ary_push(collect, rb_yield(RARRAY_PTR(ary)[i]));
}
return collect;
}
This translates to:
class Array
def collect
collect = []
self.each do |el|
collect << yield(el)
end
collect
end
end
You probably don't need to retrieve all the attributes (for example 'updated_at'), so the following may be faster:
#clients = Client.select([:name, :email, :id]).map do |client|
client.attributes.values.join(' ')
end
Added the id in case you need to link to the client.

Create an array from data received

I am trying to learn how to get data via a screen scrape and then save it to a model. So far I can grab the data. I say this as if I do:
puts home_team
I get all the home teams returned
get_match.rb #grabbing the data
require 'open-uri'
require 'nokogiri'
module MatchGrabber::GetMatch
FIXTURE_URL = "http://www.bbc.co.uk/sport/football/premier-league/fixtures"
def get_fixtures
doc = Nokogiri::HTML(open(FIXTURE_URL))
home_team = doc.css(".team-home.teams").text
end
end
Then i want to update my model
match_fixtures.rb
module MatchFixtures
class MatchFixtures
include MatchGrabber::GetMatch
def perform
update_fixtures
end
private
def update_fixtures
Fixture.destroy_all
fixtures = get_fixtures
end
def update_db(matches)
matches.each do |match|
fixture = Fixture.new(
home_team: match.first
)
fixture.save
end
end
end
end
So the next step is where I am getting stuck. First of all I need to put the home_team results into an array?
Second part is I am passing matches through my update_db method but that's not correct, what do I pass through here, the results of the home_team from my update_fixtures method or the method itself?
To run the task I do:
namespace :grab do
task :fixtures => :environment do
MatchFixtures::MatchFixtures.new.perform
end
end
But nothing is saved, but that is to be expected.
Steep learning curve here and would appreciate a push in the right direction.
Calling css(".team-home.teams").text does not return the matching DOM elements as an array, but as a single string.
In order to obtain an array of elements, refactor get fixture into something like this:
get_teams
doc = Nokogiri::HTML(open(FIXTURE_URL))
doc.css(".team-home.teams").map { |el| el.text.strip }
end
This will return an array containing the text of the elements matching your selector, stripped out of blank and new line characters. At this point you can loop over the returned array and pass each team as an argument to your model's create method:
get_teams.each { |team| Fixture.create(home_team: team) }
You could just pass the array directly to the update method:
def update_fixtures
Fixture.destroy_all
update_db(get_fixtures)
end
def update_db(matches)
matches.each {|match| Fixture.create(home_team: match.first) }
end
Or do away with the method all together:
def update_fixtures
Fixture.destroy_all
get_fixtures.each {|match| Fixture.create(home_team: match.first) }
end

Resources