Is there a possibility to write parametrized tests with Ruby+Minitest? - ruby

I've recently started working on a automated testing project, that uses Ruby+Minitest and I wonder if I can run 1 test as many times as many input data I provide.
So, I have smth like (code is under NDA so I can't provide real examples)
def test_new_registrations
result = process_new_entry(list_entries)
assert(validator_method(result), result)
end
The trick is that inside process_new_entry method there's a loop I'm glad to get rid of and just run this test as many times as many entries are there on the list_entries
From TestNG in Java+Selenium I recall a concept of using a dataprovider which passes input data inside the test method one by one.
Is there any chance a simmilar approach can be implemented here?

If you have:
class MyTest
TESTCASES = {
// input => expected
a: "a",
b: "b",
c: "c",
}
def test_testcases
TESTCASES.each do |entry, expected|
result = process_one(entry)
assert(expected, result)
end
end
end
And you really want to run each case in its own test instead, then it's just:
MyTest
TESTCASES = {
// input => expected
a: "a",
b: "b",
c: "c",
}
TESTCASES.each do |entry, expected|
define_method("test_testcase_#{entry}") do
result = process_one(entry)
assert(expected, result)
end
end
end
If you REALLY wanted to have the exact same syntax as in Java, it'd be possible to convert the above into a library so that this would work:
testcase_parameters(TESTCASES) # left as an exercise to the reader
def test_testcase(entry, expected)
assert(expected, process_one(entry))
end
But I don't see the benefit of being so indirect/abstract about it. I would suggest to stay explicit, keeping the code and execution state inspect-able and easy to debug:
class MyTest
TESTCASES = {
// input => expected
a: "a",
b: "b",
c: "c",
}
def test_testcases
results = TESTCASES.map do |entry, expected|
result = process_one(entry)
[entry, [expected, result]]
end
invalid_results = results.select { |e, (ex, res)| ex != res }
# This is a very easy-to-breakpoint place in code, to easily view all results:
# binding.pry
assert_empty invalid_results # failure cases will be printed out by test output
end
end

Related

Syntax error, unexpected tIDENTIFIER, expecting ')' Ruby

I get the following error when running a simple method that takes in a proper noun string and returns the string properly capitalized.
def format_name(str)
parts = str.split
arr = []
parts.map do |part|
if part[0].upcase
else part[1..-1].downcase
arr << part
end
end
return arr.join(" ")
end
Test cases:
puts format_name("chase WILSON") # => "Chase Wilson"
puts format_name("brian CrAwFoRd scoTT") # => "Brian Crawford Scott"
The only possibility that the above code returns a blank output is because your arr is nil or blank. And the reason your arr is blank(yes it is blank in your case) because of this line of code:
if part[0].upcase
in which the statement would always return true, because with every iteration it would check if the first element of the part string can be upcased or not, which is true.
Hence, your else block never gets executed, even if this got executed this would have returned the same string as the input because you are just putting the plain part into the array arr without any formatting done.
There are some ways you can get the above code working. I'll put two cases:
# one where your map method could work
def format_name(str)
parts = str.split
arr = []
arr = parts.map do |part|
part.capitalize
end
return arr.join(" ")
end
# one where your loop code logic works
def format_name(str)
parts = str.split
arr = []
parts.map do |part|
arr << "#{part[0].upcase}#{part[1..-1].downcase}"
end
return arr.join(" ")
end
There are numerous other ways this could work. I'll also put the one I prefer if I am using just plain ruby:
def format_name(str)
str.split(' ').map(&:capitalize)
end
You could also read more about the Open Classes concept to put this into the String class of ruby
Also, checkout camelize method if you're using rails.

How to optimize extracting data from nested hashes in ruby?

Background
I have a collection of nested hashes which present a set of parameters to define application behavior:
custom_demo_options: {
verticals: {
fashion: true,
automotive: false,
fsi: false
},
channels: {
b2b: true,
b2c: true
}
}
website_data: {
verticals: {
fashion: {
b2b: {
code: 'luma_b2b',
url: 'b2b.luma.com'
},
b2c: {
code: 'base',
url: 'luma.com'
}
}
}
}
The choices made in the custom_demo_options hash relate to data stored in the website_data hash and serve to return values from it:
data = []
collection = {}
custom_demo_options[:verticlas].each do |vertical_name, vertical_choice|
# Get each vertical selection
if vertical_choice == true
# Loop through the channels for each selected vertical
custom_demo_options[:channels].each do |channel_name, channel_choice|
# Get each channel selection for each vertical selection
if channel_choice == true
# Loop through the website data for each vertical/channel selection
website_data[:verticals].each do |site_vertical, vertical_data|
# Look at the keys of the [:website_data][:verticals] hash
# If we have a vertical selection that matches a website_data vertical...
if site_vertical == vertical_name
# For each website_data vertical collection...
vertical_data.each do |vertical_channel, channel_value|
# If we have a matching channel in the collection...
if vertical_channel == channel_name
# Add the channel's url and code to the collection hash
collection[:url] = channel_value[:url]
collection[:code] = channel_value[:code]
# Push the collection hash(es) onto the data array
data.push(collection)
}
}
}
}
}
}
}
}
The data pushed to the data array is ultimately used to create the following nginx map definition:
map $http_host $MAGE_RUN_CODE {
luma.com base;
b2b.luma.com luma_b2b;
}
As an example of the relationship between the hashes, if a user sets custom_demo_options[:channels][:b2b] tofalse, the b2b code/url pair stored in thewebsite_data` hash would be removed from the nginx block:
map $http_host $MAGE_RUN_CODE {
luma.com base;
}
Question
The above code works, but I know it's horribly inefficient. I'm relatively new to ruby, but I think this is most likely a logical challenge rather than a language-specific one.
My question is, what is the proper way to connect these hashes rather than using loops as I've done? I've done some reading on hash.select and it seems like this might be the best route, but I'd like to know: are there are other approaches I should consider that would optimize this operation?
UPDATE
I've been able to implement the first suggestion (thanks again to the poster); however, I think the second solution will be a better approach. Everything works as described; however, my data structure has changed slightly, and although I understand what the solution is doing, I'm having trouble adapting accordingly. Here's the new structure:
custom_demo_options = {
verticals: {
fashion: true,
automotive: false,
fsi: false
},
channels: {
b2b: true,
b2c: true
},
geos: [
'us_en'
]
}
website_data = {
verticals: {
fashion: {
us_en: {
b2b: {
code: 'luma_b2b',
url: 'b2b.luma.com'
},
b2c: {
code: 'base',
url: 'luma.com'
}
}
}
}
}
So, I add another level to the hashes, :geo.
I've tried to adapt the second solution has follows:
class CustomOptionsMap
attr_accessor :custom_options, :website_data
def initialize(custom_options, website_data)
#custom_options = custom_options
#website_data = website_data[:verticals]
end
def data
verticals = selected_verticals
channels = selected_channels
geos = selected_geos
# I know this is the piece I'm not understanding. How to map channels and geos accordingly.
verticals.map{ |vertical| #website_data.fetch(vertical).slice(*channels) }
end
private
def selected_geos
#custom_options[:geos].select{|_,v| v } # I think this is correct, as it extracts the geo from the array and we don't have additional keys
end
def selected_verticals
#custom_options[:verticals].select{|_,v| v }.keys
end
def selected_channels
#custom_options[:channels].select{|_,v| v }.keys
end
end
demo_configuration = CustomOptionsMap.new(custom_demo_options, website_data)
print demo_configuration.data
Any guidance on what I'm missing regarding the map statement would be very much appreciated.
Object Oriented approach.
Using OOP might be more readable and consistent in this context, as Ruby is Object Oriented language.
Introducing simple Ruby class and using activesupport module, which is extending Hash with some useful methods, same result can be achieved in the following way:
class WebsiteConifg
attr_accessor :custom_options, :website_data
def initialize(custom_options, website_data)
#custom_options = custom_options
#website_data = website_data[:verticals]
end
def data
verticals = selected_verticals
channels = selected_channels
verticals.map{ |vertical| #website_data.fetch(vertical).slice(*channels) }
end
private
def selected_verticals
#custom_options[:verticals].select{|_,v| v }.keys
end
def selected_channels
#custom_options[:channels].select{|_,v| v }.keys
end
Based on passed custom_demo_options we can select verticals and channels of only those keys, which values are set as true.
For your configuration will return
selected_verticals # [:fashion]
selected_channels # [:b2b, :b2c]
+data()
Simple public interface is iterating through all selected verticals based on the passed options and return Array of hashes for the given channels by using slice(keys).
fetch(key)
return value for the given key it is an equivalent of h[:key]
h = {a: 2, b: 3}
h.fetch(:a) # 2
h.fetch(:b) # 3
slice(key1, key2) does require activesupport
returns hash which contains passed as an arguments, keys. Method is accepting multiple arguments, as in our example we are getting an Array of those keys, we can use * splat operator to comply with this interface.
h = {a: 2, b: 3}
h.slice(:a) # {:a=>2}
h.slice(:a, :b) # {:a=>2, :b=>3}
h.slice(*[:a, :b]) # {:a=>2, :b=>3}
Usage
website_config = WebsiteConifg.new(custom_demo_options, website_data)
website_config.data
# returns
# [{:b2b=>{:code=>"luma_b2b", :url=>"b2b.luma.com"}, :b2c=>{:code=>"base", :url=>"luma.com"}}]
UPDATE
Changed relevant parts:
def data
verticals = selected_verticals
channels = selected_channels
geos = selected_geos
verticals.map do |vertical|
verticals_data = #website_data.fetch(vertical)
# in case of multiple geolocations
# collecting relevant entries of all of them
geos_data = geos.map{|geo| verticals_data.fetch(geo) }
# for each geo-location getting selected channels
geos_data.map {|geo_data| geo_data.slice(*channels) }
end.flatten
end
private
# as `website_data' hash is using symbols, we need to covert string->sym
def selected_geos
#custom_options[:geos].map(&:to_sym)
end
def selected_verticals
selected_for(:verticals).keys
end
def selected_channels
selected_for(:channels).keys
end
def selected_for(key)
#custom_options[key].select{|_,v| v }
end
Easiest way to understand what kind of output(data) you have on each of the steps in the each(map) iterator, would be to place there debugger
like: pry, byebug.
Say you have key = :foo and hash = { foo: 1, bar: 2 } - you want to know the hash's value for that key.
The approach you're using here is essentially
result = nil
hsh.each { |k,v| result = v if k == :foo }
But why do that when you can simply say
result = hsh[:foo]
It seems like you understand how hashes can be iterable structures, and you can traverse them like arrays. But you're overdoing it, and forgetting that hashes are indexed structures. In terms of your code I would refactor it like so:
# fixed typo here: verticlas => verticals
custom_demo_options[:verticals].each do |vertical_name, vertical_choice|
# == true is almost always unnecessary, just use a truthiness check
next unless vertical_choice
custom_demo_options[:channels].each do |channel_name, channel_choice|
next unless channel_choice
vertical_data = website_data[:verticals][site_vertical]
channel_value = vertical_data[channel_name]
# This must be initialized here:
collection = {}
collection[:url] = channel_value[:url]
collection[:code] = channel_value[:code]
data.push(collection)
end
end
You can see that a lot of the nesting and complexity is removed. Note that I am initializing collection at the time it has attributes added to it. This is a little too much to go into here but I highly advise reading up on mutability in Ruby. You're current code will probably not do what you expect it to because you're pushing the same collection hash into the array multiple times.
At this point, you could refactor it into a more functional-programming style, with some chained methods, but I'd leave that exercise for you

Use a block to set variables in parent scope

The gem binding_of_caller has an example for how to set a variable in a parent scope:
(this just pasted from their readme)
def a
var = 10
b
puts var
end
def b
c
end
def c
binding.of_caller(2).eval('var = :hello')
end
a()
# OUTPUT
# => hello
This is useful, but it's limited by the need to do all variable initialization in a string.
I gave it a little thought and realized that I could use YAML to serialize/deserialize objects.
Take, for example, the following example:
def c
value = YAML.dump [ { a: "b" } ]
binding.of_caller(2).eval("var = YAML.load('#{value}')")
end
a()
# => {a: "b"}
This is cool, but it'd be better if I could avoid serialization altogether and just write a proper do; end; block like so:
# doesnt work
def c
binding.of_caller(2).eval do
# ideally this would set the variable named "var" in the scope of method "a"
var = [ { a: "b" } ]
end
end
How can I accomplish the functionality of this last example? I don't need to use binding_of_caller if there's another way.
This is the best I could do and, I suspect (though I'd truly love to be proven wrong), the best you'll find short of writing your own C extension a la binding_of_caller:
require 'binding_of_caller'
module BindingExtensionEvalBlock
def eval_block(&block)
eval("ObjectSpace._id2ref(%d).call(binding)" % block.object_id)
end
end
class ::Binding
include BindingExtensionEvalBlock
end
The magic is here, of course:
eval("ObjectSpace._id2ref(%d).call(binding)" % block.object_id)
We get the object ID of the Proc and then, in our Binding#eval, use ObjectSpace#_id2ref to retrieve it from wherever it is in memory and call it, passing in the local binding.
Here it is in action:
def a
var = 10
b
puts var
end
def b
c
end
def c
binding.of_caller(2).eval_block do |bnd|
bnd.local_variable_set(:var, [ { a: "b" } ])
end
end
a # => {:a=>"b"}
As you can see, instead of var = [ { a: "b" } ] in our block we must do bnd.local_variable_set(:var, [ { a: "b" } ]). There's no way to change the binding of a block in Ruby, so we have to pass in a binding (bnd) and call Binding#local_variable_set.

How to handle thread returns Ruby

I have a ruby script in which I'm parsing a large csv file. Ihave everything handled and working fairly well, except for how to deal with the thread's return values. I have:
length = (ses.size/4).ceil
ses.each_slice(length) do |x|
threads << Thread.new { a,b = splat x }
end
threads.each { |thr|
thr.join
}
'splat' returns to temp files that need to appended to the output files out1 and out2. I'm stumbling on where exactly to do that/how to get that information. If someone could point me in the right direction that'd be great.
Two things, first, when you pass the 'x' into the thread, it's safer to make it thread-local by changing this:
threads << Thread.new { a,b = splat x }
Into this:
threads << Thread.new(x) { |x| a,b = splat x }
Next, to get the return value out, you join using :value.
So here's a quick demo I whipped up:
dummy = [
['a.txt', 'b.txt'],
['c.txt', 'd.txt'],
['e.txt', 'f.txt'],
['g.txt', 'h.txt'],
['i.txt', 'j.txt'],
['k.txt', 'l.txt']
]
threads = dummy.map do |pair|
Thread.new(pair) { |val| val }
end
vals = threads.map(&:value) # equiv. to 'threads.map { |t| t.value }'
puts vals.inspect
Crib off that and it should get you where you want to go.

Unexpected hash's behavior

I'm newbie.
Help me, please.
I write Puppet function
Piece of code :
n_if={}
over_if = arguments[1]
over_if.each do |kk,vv|
weth={}
puts kk,vv,weth
weth = arguments[0]
weth['in_vlan'] = vv['in_vlan']
weth['options']['MTU'] = vv['mtu']
n_if['eth'+ kk.to_s]=weth
end
Data readed from 2 files, and passed into arguments[0] and arguments[1] respectively:
# template of ethernet interfaces
eth_:
method: "static"
family: "inet"
ip: ""
netmask: "255.255.0.0"
onboot: true
options:
MTU: ""
in_vlan: ""
# values for include into ethernet interfaces
eth_values:
0:
mtu: 1500
in_vlan: 15
1:
mtu: 9000
in_vlan: 125
I expect get hash with keys 'eth0' and 'eth1' as follow:
eth1methodstaticfamilyinetin_vlan125ipnetmask255.255.0.0onboottrueoptionsMTU9000eth0methodstaticfamilyinetin_vlan15ipnetmask255.255.0.0onboottrueoptionsMTU1500
But I get :
eth1methodstaticfamilyinetin_vlan125ipnetmask255.255.0.0onboottrueoptionsMTU9000eth0methodstaticfamilyinetin_vlan125ipnetmask255.255.0.0onboottrueoptionsMTU9000
What is my mistake?
First, some comments:
Your code is not indented in a way that most others do it, which makes it hard for others to help you. It should look something like this:
n_if={}
over_if = arguments[1]
over_if.each do |kk,vv|
weth={}
puts kk,vv,weth
weth = arguments[0]
weth['in_vlan'] = vv['in_vlan']
weth['options']['MTU'] = vv['mtu']
n_if['eth'+ kk.to_s]=weth
end
Perhaps your variable names make sense to you, but they don't make sense to me. What is n_if, weth, over_if, kk and vv?
You assign weth to be a hash inside your each, and then you assign it to be something else. What are you really trying to do?
You say that arguments[0] and arguments[1] are data read in from files. How are these read in? Are these YAML files? It would be helpful if you would include code to actually reproduce your problem. Pare it down to the essentials.
In Ruby it is generally more idiomatic and performant not to concatenate strings, but to use string interpolation:
n_if["eth#{kk}"] = weth
Now, some answers:
My guess is that your setup holds data like this:
arguments = {
"eth_"=>{
"method"=>"static",
"family"=>"inet",
"ip"=>"",
"netmask"=>"255.255.0.0",
"onboot"=>true,
"options"=>{"MTU"=>""},
"in_vlan"=>""
},
"eth_values"=>{
0=>{"mtu"=>1500, "in_vlan"=>15},
1=>{"mtu"=>9000, "in_vlan"=>125}
}
}
arguments[0] = arguments['eth_']
arguments[1] = arguments['eth_values']
I believe (based on many guesses as to what you have and what you may want) that your problem is this combination:
weth={}
weth=arguments[0]
I think your intent here is to say "weth is a hash type of object; now fill it with values from arguments[0]". What those lines actually say is:
Set weth to an empty hash.
Nevermind, throw away that empty hash and set weth to the same object as arguments[0].
Consequently, each time through the loop you are modifying the same hash with weth. Instead, I think you want to duplicate the hash for weth. Does the following modified code give you what you need?
n_if={}
over_if = arguments[1]
over_if.each do |kk,vv|
weth = arguments[0].dup
weth['in_vlan'] = vv['in_vlan']
weth['options']['MTU'] = vv['mtu']
n_if["eth#{kk}"]=weth
end
require 'pp' # for nice wrapping inspection
pp n_if
#=> {"eth0"=>
#=> {"method"=>"static",
#=> "family"=>"inet",
#=> "ip"=>"",
#=> "netmask"=>"255.255.0.0",
#=> "onboot"=>true,
#=> "options"=>{"MTU"=>9000},
#=> "in_vlan"=>15},
#=> "eth1"=>
#=> {"method"=>"static",
#=> "family"=>"inet",
#=> "ip"=>"",
#=> "netmask"=>"255.255.0.0",
#=> "onboot"=>true,
#=> "options"=>{"MTU"=>9000},
#=> "in_vlan"=>125}}
If not, please edit your question with more details on what you ACTUALLY have (hint: p arguments and show us the result) and what you really want as the result.
Edit: For fun, here's a functional transformation instead. It is left as an exercise to the reader to understand how it works and level-up their functional programming skills. Note that I have modified eth_values to match the hierarchy of the template so that simple merging can be applied. I've left the "MTU"=>"" and "in_vlan"=>"" entries in, but note that they are not necessary for the code to work, you could delete both (and the resulting "options"=>{}) and achieve the same result.
args = {
"eth_"=>{
"method"=>"static",
"family"=>"inet",
"ip"=>"",
"netmask"=>"255.255.0.0",
"onboot"=>true,
"options"=>{"MTU"=>""},
"in_vlan"=>""
},
"eth_values"=>{
0=>{"options"=>{"MTU"=>1500}, "in_vlan"=>15},
1=>{"options"=>{"MTU"=>9000}, "in_vlan"=>125}
}
}
n_if = Hash[
args['eth_values'].map do |num,values|
[ "eth#{num}",
args['eth_'].merge(values) do |k,v1,v2|
if v1.is_a?(Hash) and v2.is_a?(Hash) then
v1.merge(v2)
else
v2
end
end ]
end
]
pp n_if #=> Same result as in the previous code.

Resources