rspec to stub several File.exists? calls - ruby

I have a function with several File.exists? calls:
def create(one_filename, other_filename)
raise Error, "One error" if File.exists?(one_filename)
raise Error, "Other error" unless File.exist?(other_filename)
.....
end
But I don't know the way to stub the second error. The spec is something as:
it "should not create an existing file" do
File.stub(:exists?).and_return(true)
expect {
subject.create('whatever')
}.to raise_error("One error")
end
it "should has to exists the other filename" do
File.stub(:exists?).and_return(false)
expect {
subject.create('whatever.yaml')
}.to raise_error("Other error")
end
For the second spec (it "should has to exists the other filename") the File exists? stub raises the first check.
What is the best way to spec the two raises?

To return multiple values for multiple calls, simply do:
File.stub(:exists?).and_return(false, true)
Or, preferably, use the new syntax:
Allow(File).to receive(:exists?).and_return(false, true)
Another option would be to stub the method according to its input variables:
Allow(File).to receive(:exists?).with(one_filename).and_return(false)
Allow(File).to receive(:exists?).with(other_filename).and_return(true)
This has the added value of actually testing the behavior rather than the implementation.

Actually
Mocking calls to FileUtils or File means tightly coupling tests with the implementation.
fakefs
Just calling the following code before your stub should be enough.
allow(File).to receive(:exist?).and_call_original
source

Related

How do you test custom Bugsnag meta_data in Ruby?

How do you test custom Bugsnag meta_data (in Ruby, with Rspec)?
The code that I want to test:
def do_something
thing_that_could_error
rescue => e
Bugsnag.notify(e) do |r|
r.meta_data = { my_extra_data: "useful info" }
end
end
The test I want to write:
context "when there's an error" do
it "calls Bugsnag with my special metadata" do
expect(Bugsnag).to receive(:notify) # TODO test meta_data values contain "my useful info"
expect do
do_something() # exception is thrown and rescued and sent to Bugsnag
end.not_to raise_error
end
end
I am using:
Ruby 2.6.6
Rspec 3.9.0
Bugsnag 6.17.0 https://rubygems.org/gems/bugsnag
The data inside of the meta_data variable is considerably more complicated than in this tiny example, which is why I want to test it. In a beautiful world, I would extract that logic to a helper and test the helper, but right now it is urgent and useful to test in situ.
I've been looking at the inside of the Bugsnag gem to figure this out (plus some Rspec-fu to capture various internal state and returned data) but at some point it's a good idea to ask the internet.
Since the metadata is complicated, I'd suggest simplifying it:
def do_something
thing_that_could_error
rescue => e
Bugsnag.notify(e) do |r|
r.meta_data = error_metadata(e, self, 'foo')
end
end
# I assumed that you'd like to pass exception and all the context
def error_metadata(e, object, *rest)
BugsnagMetadataComposer.new(e, object, *rest).metadata
end
So now you can have a separate test for BugsnagMetadataComposer where you have full control (without mocking) over how you initialize it, and test for metadata output.
Now you only have to test that BugsnagMetadataComposer is instantiated with the objects you want, metadata is called and it returns dummy hash:
let(:my_exception) { StandardError.new }
let(:mock_metadata) { Hash.new }
before do
# ensure thing_that_could_error throws `my_exception`
expect(BugsnagMetadataComposer)
.to receive(new)
.with(my_exception, subject, anything)
.and_return(mock_metadata)
end
And the hard part, ensure that metadata is assigned. To do that you can cheat a little and see how Bugsnag gem is doing it
Apparently there's something called breadcrumbs:
let(:breadcrumbs) { Bugsnag.configuration.breadcrumbs }
Which I guess has all the Bugsnag requests, last one on top, so you can do something similar to https://github.com/bugsnag/bugsnag-ruby/blob/f9c539670c448f7f129a3f8be7d412e2e824a357/spec/bugsnag_spec.rb#L36-L40
specify do
do_something()
expect(breadcrumbs.last.metadata).to eq(expected_metadata)
end
And for clarity, the whole spec would look a bit like this:
let(:my_exception) { StandardError.new }
let(:mock_metadata) { Hash.new }
before do
# ensure thing_that_could_error throws `my_exception`
expect(BugsnagMetadataComposer)
.to receive(new)
.with(my_exception, subject, anything)
.and_return(mock_metadata)
end
specify do
do_something()
expect(breadcrumbs.last.metadata).to eq(expected_metadata)
end

What is the correct way to define an alias to `after` in rspec?

Normally the before and after hooks are assumed to be "initializing" and "cleanup" code respectively. They are supposed to happen "outside of the tests themselves".
I find myself in a situation in which I want to use after as the last step of all the tests in a context. But since after is usually meant to be "cleanup", I am afraid that my tests won't be very explicit. Here's a sample:
describe "when removing" do
let!(:request) do
stub_request(:delete, "http://localhost:4567/containers/#{container.id}").
to_return(status: 200)
end
# returns an http response
subject { client.remove(container.id) }
it { should be }
it { should include('id' => container.id) }
after { expect(request).to have_been_made }
end
I would like to rename that last after to something more explicit, like invariant, to indicate that it is part of the test. I have tried doing this on my spec helper:
# spec_helper.rb
Rspec.configure do |c|
...
end
RSpec::Core::Hooks.class_eval do
alias_method :invariant, :after
end
requiring spec_helper does not seem to throw any errors, but when I run the tests replacing after with invariant I get "undefined method 'invariant' for #<Class:0x007f9872b46588> (NoMethodError)" when running the tests.
It seems there is no easy way to do this, just use before/after

Rspec Ruby Mocking

I would like to achieve 100% coverage on a module. My problem is that there is a variable (called data) within a method which I am trying to inject data in to test my exception handling. Can this be done with mocking? If not how can i fully test my exception handling?
module CSV
module Extractor
class ConversionError < RuntimeError; end
class MalformedCSVError < RuntimeError; end
class GenericParseError < RuntimeError; end
class DemoModeError < RuntimeError; end
def self.open(path)
data = `.\\csv2text.exe #{path} -f xml --xml_output_styles 2>&1`
case data
when /Error: Wrong input filename or path:/
raise MalformedCSVError, "the CSV path with filename '#{path}' is malformed"
when /Error: A valid password is required to open/
raise ConversionError, "Wrong password: '#{path}'"
when /CSVTron CSV2Text: This page is skipped when running in the demo mode./
raise DemoModeError, "CSV2TEXT.exe in demo mode"
when /Error:/
raise GenericParseError, "Generic Error Catch while reading input file"
else
begin
csvObj = CSV::Extractor::Document.new(data)
rescue
csvObj = nil
end
return csvObj
end
end
end
end
Let me know what you think! Thanks
===================== EDIT ========================
I have modified my methods to the design pattern you suggested. This method-"open(path)" is responsible for trapping and raising errors, get_data(path) just returns data, That's it! But unfortunately in the rspec I am getting "exception was expected to be raise but nothing was raised." I thought maybe we have to call the open method from your stub too?
This is what I tried doing but still no error was raised..
it 'should catch wrong path mode' do
obj = double(CSV::Extractor)
obj.stub!(:get_data).and_return("Error: Wrong input filename or path:")
obj.stub!(:open)
expect {obj.open("some fake path")}.to raise_error CSV::Extractor::MalformedCSVError
end
Extract the code that returns the data to a separate method. Then when you test open you can stub out that method to return various strings that will exercise the different branches of the case statement. Roughly like this for the setup:
def self.get_data(path)
`.\\csv2text.exe #{path} -f xml --xml_output_styles 2>&1`
end
def self.open(path)
data = get_data(path)
...
And I assume you know how to stub methods in rspec, but the general idea is like this:
foo = ...
foo.stub(:get_data).and_return("Error: Wrong input filename or path:")
expect { foo.get_data() }.to raise_error MalformedCSVError
Also see the Rspec documentation on testing for exceptions.
Problem with testing your module lies in the way you have designed your code. Think about splitting extractor into two classes (or modules, it's matter of taste -- I'd go with classes as they are a bit easier to test), of which one would read data from external system call, and second would expect this data to be passed as an argument.
This way you can easily mock what you currently have in data variable, as this would be simply passed as an argument (no need to think about implementation details here!).
For easier usage you can later provide some wrapper call, that would create both objects and pass one as argument to another. Please note, that this behavior can also be easily tested.

Is there a way to mock/stub "puts" in Rails

I am printing some custom messages in my application using the puts command. However, I do not want these to be appearing in my Test Output. So, I tried a way to stub puts as shown below. But it still outputs my messages. What am I doing wrong ?
stubs(:puts).returns("") #Did not work out
Object.stubs(:puts).returns("") #Did not work out either
puts.stubs.returns "" #Not working as well
Kernel.stubs(:puts).returns "" #No luck
I am using Test::Unit
You probably need to stub it on the actual instance that calls puts. E.g. if you're calling puts in an instance method of a User class, try:
user = User.new
user.stubs(:puts)
user.some_method_that_calls_puts
This similarly applies to when you're trying to test puts in the top-level execution scope:
self.stubs(:puts)
What I would do is define a custom log method (that essentially calls puts for now) which you can mock or silence in test quite easily.
This also gives you the option later to do more with it, like log to a file.
edit: Or if you really want to stub puts, and you are calling it inside an instance method for example, you can just stub puts on the instance of that class.
Using Rails 5 + Mocha: $stdout.stubs(puts: '')
So the comments to the original post point to the answer:
Kernel.send(:define_method, :puts) { |*args| "" }
Instead of silencing all output, I would only silence output from the the particular objects that are putsing during your tests.
class TestClass
def some_method
...
puts "something"
end
end
it "should do something expected" do
TestClass.send(:define_method, :puts) { |*args| "" }
test_class.some_method.should == "abc123"
end

How do you stub a file.read that happens inside a File.open block?

How do I stub a file.read call so that it returns what I want it to? The following does not work:
def write_something
File.open('file.txt') do |f|
return contents = f.read
end
end
# rspec
describe 'stub .read' do
it 'should work' do
File.stub(:read) { 'stubbed read' }
write_something.should == 'stubbed read'
end
end
It looks like the stub is being applied to the File class and not the file instance inside my block. So File.read returns stubbed read as expected. But when I run my spec it fails.
I should note that File.open is just one part of Ruby’s very large I/O API, and so your test is likely to be very strongly coupled to your implementation, and unlikely to survive much refactoring. Further, one must be careful with “global” mocking (i.e. of a constant or all instances) as it can unintentionally mock usages elsewhere, causing confusing errors and failures.
Instead of mocking, consider either creating an actual file on disk (using Tempfile) or using a broader I/O mocking library (e.g. FakeFS).
If you still wish to use mocking you can somewhat safely stub File.open to yield a double (and only when called with the correct argument):
file = instance_double(File, read: 'stubbed read')
allow(File).to receive(:open).and_call_original
allow(File).to receive(:open).with('file.txt') { |&block| block.call(file) }
or, somewhat dangerously, stub all instances:
allow_any_instance_of(File).to receive(:read).and_return('stubbed read')
The main point is to make File.open to return an object that will respond to read with the content you want, here's the code:
it "how to mock ruby File.open with rspec 3.4" do
filename = 'somefile.txt'
content = "this would be the content of the file"
# this is how to mock File.open:
allow(File).to receive(:open).with(filename, 'r').and_yield( StringIO.new(content) )
# you can have more then one of this allow
# simple test to see that StringIO responds to read()
expect(StringIO.new(content).read).to eq(content)
result = ""
File.open('somefile.txt', 'r') { |f| result = f.read }
expect(result).to eq(content)
end
This is how I'd do it
describe 'write_something' do
it 'should work' do
file_double = instance_double('File')
expect(File).to receive(:open).with('file.txt').and_yield(file_double)
expect(file_double).to receive(:read).and_return('file content')
content = write_something
expect(content).to eq('file content')
end
end

Resources