Test nested File.open - ruby

I need to test a file open operation. I am able to test the first operation but not the second.
File.open("#{TemplateFile.fixture_path}/#{#template_file}") do |input_file|
template = ERB.new(input_file.read)
File.open("#{#project_name}/#{#destination_file}", 'w') do |output_file|
output_file.puts template.result binding
end
end
end
I am using this code:
module Pod
describe TemplateFile do
it "opens the template" do
dict = {"README.md.erb" => "README.md"}
File.expects(:open).with("#{TemplateFile.fixture_path}/README.md.erb")
File.expects(:open).with("Sample/README.md.erb", 'w')
TemplateFile.new(dict, "Sample")
end
end
end
But I am getting an error:
unsatisfied expectations:
- expected exactly once, not yet invoked: File.open('/README.md.erb', 'w')
satisfied expectations:
- expected exactly once, invoked once: File.open('/lib/pod/command/../../../fixtures/README.md.erb')
It seems that Mocha is not geeting the second File.open.

The reason is because expects verifies the call would happen but doesn't actually let it go through. So what's in the block doesn't get run.
However, beyond just telling you why it's not working, I also wanted to point out what you are doing is probably not what you want to do.
What you likely want to do do is:
template = ERB.new(File.read("#{TemplateFile.fixture_path}/#{#template_file}"))
File.open("#{#project_name}/#{#destination_file}", 'w') do |output_file|
output_file.puts template.result binding
end
You don't need that nesting.
Then when testing what you want to do to verify your the correct file is read is:
File.expects(:read).with("#{TemplateFile.fixture_path}/README.md.erb").returns(some_known_fixture)
The returns part says when it does get this read method with the specified argument I want you to return this known thing so that template will have a good value for the rest of the code.

Related

Unable to set node attribute in runtime and reference it in chef template

I am getting template error due to backend.key=<%= node['key']%> used in source key.properties.erb doesn’t have a value while running shellout.
Error : Chef::Mixin::Template::TemplateError - undefined method `[]' for nil:NilClass
I have a ruby block to get the output of the file cat /tmp/key.txt and assigning as a node value.
Ruby block :
ruby_block "Get_key" do
block do
#tricky way to load this Chef::Mixin::ShellOut utilities
Chef::Resource::RubyBlock.send(:include, Chef::Mixin::ShellOut)
command = 'cat /tmp/key.txt'
command_out = shell_out(command)
node.set['key'] = command_out.stdout
end
action :create
end
Erb :
backend.key=<%= node['key']%>
There is no need to shell_out to read the contents of a file. Try this instead:
ruby_block "Get_key" do
only_if { node['key'] == "" }
block do
node.set['key'] = File.read('/tmp/key.txt')
end
end
But I think your actual problem is somewhere else. The error message indicates that node is nil within your template, which is pretty unusual.
So either I am blind and you really have some typo in the posted template line, or you simplified your code example in such a way that it hides your error. I assume your real template looks more like
backend.key=<%= node['foo']['key']%>
and foo not being an array. Check that.
Please don't use this pattern. It's slow and puts extra data in your node object which takes up space and RAM and makes you search index sad. What you want is this:
template "whatever" do
# Other stuff ...
variables my_file: lazy { IO.read('/tmp/key.txt') }
end
That will delay the read until converge time.

RSpec mocking, `name` not available from within an example group

I have the following Ruby code:
def report_deviation(departure)
deviation = departure.fetch('Dev')
trip = departure.fetch('Trip')
run_id = trip.fetch('RunId')
headsign = trip.fetch('InternetServiceDesc')
timestamp = Time.now.strftime '%l:%M %P'
FileUtils.mkdir 'log' unless File.directory? 'log'
File.open DAILY_LOG_FILE, 'a' do |file|
file.puts "#{timestamp}, #{name}: Run #{run_id} (#{headsign}), deviation #{deviation}"
end
end
Tested by the following RSpec code:
describe 'report_deviation' do
let(:departure) { double }
let(:trip) { double }
let(:file) { double }
it 'appends to a log file with the correct entry format' do
expect(departure).to receive(:fetch).with('Trip').and_return trip
expect(departure).to receive(:fetch).with('Dev').and_return 'DEVIATION'
expect(trip).to receive(:fetch).with('RunId')
.and_return 'RUN'
expect(trip).to receive(:fetch).with('InternetServiceDesc')
.and_return 'HEADSIGN'
stub_const 'DeviationValidator::DAILY_LOG_FILE', :log_file
expect(File).to receive(:open).with(:log_file, 'a').and_yield file
timestamp = '12:00 pm: Run RUN (HEADSIGN), deviation DEVIATION'
expect(file).to receive(:puts).with timestamp
Timecop.freeze(Time.new 2017, 7, 31, 12) { report_deviation(departure) }
end
end
But when I run I receive the failure message:
`name` is not available from within an example (e.g. an `it` block) or from constructs that run in the scope of an example (e.g. `before`, `let`, etc). It is only available on an example group (e.g. a `describe` or `context` block).
The word name isn't written anywhere in here, and if I remove the final line of the test (which invokes the actual code) I get the test failures I would expect for unsatisfied exceptions. I normally would boil my code down to the pieces that are causing the error, but I have no idea what's causing the error.
For what it's worth, the specific line number mentioned in the backtrace is the file.puts within the File.open block - but I don't understand why that should cause a failure. I've set up test doubles such that those objects are nothing special - File receives open and yields file, whose only job is to listen for receiving puts with the string I expect. So what piece of code is calling what happens to be a keyword RSpec method name?
The problem is from rspec gem, if you are using Rails 6 you need to use gem 'rspec-rails', '~> 4.1.0'
name is not a keyword RSpec method, it's a method that report_deviation is trying to call
file.puts "#{timestamp}, #{name}: Run #{run_id} (#{headsign}), deviation #{deviation}"
but the method is not defined.
You need to define the name method in the class where report_deviation is defined. Or, if report_deviation is defined and used in the spec file, add a simple variable called name:
describe 'report_deviation' do
let(:departure) { double }
let(:trip) { double }
let(:file) { double }
let(:name) { "simple name" }
...
`name` is not available from within an example (e.g. an `it` block) [...]
I had a similar problem today. The final solution for the issue for now with a monkeypatch to go back to using method_name.
Create config/initializers/monkeypatches.rb file and fill inside with the following lines.
# config/initializers/monkeypatches.rb
#
# This fixes what seems to be a bug introduced by
# https://github.com/rails/rails/pull/37770
# "Modify ActiveRecord::TestFixtures to not rely on AS::TestCase:"
#
module ActiveRecord::TestFixtures
def run_in_transaction?
use_transactional_tests &&
!self.class.uses_transaction?(method_name) # this monkeypatch changes `name` to `method_name`
end
end
Credits: https://github.com/graphql-devise/graphql_devise/issues/42

Can you pass a block of code that returns an error to a method?

I often find myself dealing with these kind of scenarios:
require 'nokogiri'
require "open-uri"
url = "https://www.random_website.com/contains_info_I_want_to_parse"
nokodoc = Nokogiri::HTML(open(url))
# Let's say one of the following line breaks the ruby script
# because the element I'm searching doesn't contain an attribute.
a = nokodoc.search('#element-1').attribute('href').text
b = nokodoc.search('#element-2').attribute('href').text.gsub("a", "A")
c = nokodoc.search('#element-3 h1').attribute('style').text.strip
What happens is that I'll be creating about 30 variables all searching for different elements in a page, and I'll be looping that code over multiple pages. However, a few of these pages may have an ever-so-slightly different layout and won't have one of those div. This will break my code (because you can't call .attribute or .gsub on nil for example). But I can never guess which line before-hand.
My go-to solution is usually surround each line with:
begin
line #n
rescue
puts "line #n caused an error"
end
I'd like to be able to do something like:
url = "https://www.random_website.com/contains_info_I_want_to_parse"
nokodoc = Nokogiri::HTML(open(url))
catch_error(a, nokodoc.search('#element-1').attribute('href').text)
catch_error(b, nokodoc.search('#element-2').attribute('href').text.gsub("a", "A"))
catch_error(c, nokodoc.search('#element-3 h1').attribute('style').text.strip)
def catch_error(variable_name, code)
begin
variable_name = code
rescue
puts "Code in #{variable_name} caused an error"
end
variable_name
end
I know that putting & before each new method works:
nokodoc.search('#element-1')&.attribute('href')&.text
But I want to be able to display the error with a 'puts' in my terminal to see when my code gives an error.
Is it possible?
You can't pass your code as a regular argument to a method because it'll be evaluated (and raise an exception) before it gets passed to your catch_error method. You could pass it as a block--something like
a = catch_error('element_1 href text') do
nokodoc.search('#element-1').attribute('href').text
end
def catch_error(error_description)
yield
rescue
puts "#{error_description} caused an error"
end
Note that you can't pass a to the method as variable_name: it hasn't been defined anywhere before calling that method, so you'll get an undefined local variable or method error. Even if you define a earlier, it won't work correctly. If your code works without raising an exception, the method will return the right value but the value won't get stored anywhere outside the method scope. If there is an exception, variable_name will have whatever value a had before the method (nil if you defined it without setting it), so your error message would output something like Code in caused an error. That's why I added an error_description parameter.
You could also try logging the message and backtrace if you didn't want to have to specify an error description every time.
a = catch_error(nokodoc) do |doc|
doc.search('#element-1').attribute('href').text
end
def catch_error(doc)
yield doc
rescue => ex
puts doc.title # Or something else that identifies the document
puts ex.message
puts ex.backtrace.join("\n")
end
I made one additional change here: passing the document in as a parameter so that rescue could easily log something that identifies the document, in case that's important.

Stock quote gem - retrieval for nonexistent ticker yields nomethod error

I am using the "stock quote" gem (https://github.com/tyrauber/stock_quote) to retrieve stock prices based on user input tickers. While I have a ticker list that is up-to-date, there are some circumstances where the search yields no results. I have this in my code to get the quote:
#companyname = StockQuote::Stock.quote(#ticker).company
#exchange = StockQuote::Stock.quote(#ticker).exchange
#price = StockQuote::Stock.quote(#ticker).last
And it yields this when #ticker = "AKO-A"
undefined method `attributes' for nil:NilClass
file: stock.rb location: block in parse line: 90
Is there anyway to avoid this nomethoderror by making my code more robust (if error then "blank")? Sorry, I am relatively new to ruby and would appreciate any help to point me in the right direction.
Yeah, the problem was definitely with the gem. It was assuming the symbol was accurate and wasn't properly parsing responses for bad symbols.
Sloppy. Rewrote the classes for cleaner code and greater stability. Added in a response_code instance method, which returns 200 or 404, depending upon the validity of the response. Also, a success? or failure? instance method. And, better spec coverage.
Version bumped, and pushed to rubygems.
This is a very common condition with Ruby code, and a common idiom to return nil on a failed search.
However this specific gem is a little flaky when it fails to get a good search result. You can protect yourself against it failing by using a begin ... rescue block.
begin
stock_quote = StockQuote::Stock.quote(#ticker)
rescue StandardError
stock_quote = nil
end
if stock_quote
#companyname = stock_quote.company
#exchange = stock_quote.exchange
#price = stock_quote.last
end
This might not be ideal program flow for you, so you may need to adapt this.
Note StandardError is what gets rescued by default, I didn't need to write that. You could also put NoMethodError in your situation, and usually you want to restrict rescuing exceptions to specific parts of code where you know how to recover from the error, and also only to the types of errors where you are confident that your handling code is doing the right thing.
Here is an example on how use rescue to get around the nonexistent stock symbol problem
require 'stock_quote'
class StockClass
def self.symbol_check(symbol)
StockQuote::Stock.quote(symbol).symbol
end
def self.price_by_symbol(symbol)
StockQuote::Stock.quote(symbol).latest_price
end
def self.write_price_by_symbol(symbol, price)
filename = "#{symbol}.csv"
todays_date = Time.now.strftime('%Y-%m-%d')
File.open(filename, "a") do |file|
file << "#{todays_date}, #{price}\n"
end
end
end
def stock_price_selector(*symbol_array)
symbol_array.each do |stock_name|
begin
stock_check = StockClass.symbol_check(stock_name)
rescue NoMethodError
puts "#{stock_name} is a bogus ticker symbol"
else
stock_price = StockClass.price_by_symbol(stock_name)
stock_written = StockClass.write_price_by_symbol(stock_name, stock_price)
end
end
end
stock_price_selector('AAPL', 'APPL', 'MSFT', 'GOOG')
This will skip the bogus symbol 'APPL' and work for the legtimate ticker symbols.

Isolating specs to one specific call of a method

I am writing a spec for an object (Sample) that calls another object's method (IO#delete) a number of time.
I want to isolate the tests of this method, however when I do this:
class Sample
def delete_them
io.delete "file1"
io.delete "folder1"
end
end
describe Sample do
let(:io) { stub.as_null_object }
subject { Sample.new.tap { |s| s.stub(:io).and_return(io) }}
it "deletes file1" do
io.should_receive(:delete).with("file1")
subject.delete_them
end
it "deletes folder1" do
io.should_receive(:delete).with("folder1")
subject.delete_them
end
end
If I call multiple methods it's not a problem because I am using the null object pattern. However, in this case when I execute the second test, it complains:
1) Sample instance methods#delete_them deletes folder1
Failure/Error: io.should_receive(:delete).with("folder1")
Stub received :delete with unexpected arguments
expected: ("folder1")
got: ("file1")
Is there a way to indicate that all the calls must be ignored except the one I am trying to make sure is being done?
This code compiles fine. It was another issue that was causing my problem.

Resources