How can I appropriately mock out a method that returns yield? - ruby

It's fairly common in Ruby for methods that take blocks to look like this:
class File
def open(path, mode)
perform_some_setup
yield
ensure
do_some_teardown
end
end
It's also fairly idiomatic for a method to look like this:
def frobnicate
File.open('/path/to/something', 'r') do |f|
f.grep(/foo/).first
end
end
I want to write a spec for this that doesn't hit the filesystem, which ensures it pulls the right word out of the file, something like:
describe 'frobnicate' do
it 'returns the first line containing the substring foo' do
File.expects(:open).yields(StringIO.new(<<EOF))
not this line
foo bar baz
not this line either
EOF
expect(frobnicate).to match(/foo bar baz/)
end
end
The problem here is that, by mocking out the call to File.open, I've also removed its return value, which means that frobnicate will return nil. If I were to add something like File.returns('foo bar baz') to the chain, however, I'd end up with a test that doesn't actually hit any of the code I'm interested in; the contents of the block in frobnicate could do anything and the test would still pass.
How might I appropriately test my frobnicate method without hitting the filesystem? I'm not particularly attached to any particular testing framework, so if your answer is "use this awesome gem that'll do it for you" then I'm OK with that.

It seems like you just need to mock the call to File a little differently. I was getting syntax errors running your code as-is, so I'm not sure what version of RSpec you're on, but if you're on 3.x this will do the job:
frobnicate_spec.rb
gem 'rspec', '~> 3.4.0'
require 'rspec/autorun'
RSpec.configure do |config|
config.mock_with :rspec
end
def frobnicate
File.open('/path/to/something', 'r') do |f|
f.grep(/foo/).first
end
end
RSpec.describe 'frobnicate' do
it 'returns the first line containing the substring foo' do
allow(File).to receive(:open).and_call_original
allow(File).to receive(:open).and_yield StringIO.new <<-EOF
not this line
foo bar baz
not this line either
EOF
expect(frobnicate).to match(/foo bar baz/)
end
end
Invoke with ruby frobnicate_spec.rb so we can use a specified RSpec version.
Source: RSpec Mocks expecting messages and yielding responses

Using minitest it could be done like I post below. I have added the whole runnable file, so you can test it from the command line with ruby -Ilib:test test_file.rb:
def frobnicate
found_string = nil
File.open('/path/to/something', 'r') do |f|
found_string = f.grep(/foo/).first
end
found_string
end
class FrabnicateTest < Minitest::Test
def test_it_works
mock_file = StringIO.new(%(
not this line
foo bar baz
not hthis line either
))
search_result = nil
File.stub(:open, nil, mock_file) do
search_result = frobnicate
end
assert_match(/foo bar baz/, search_result)
end
end

Related

Ruby with RSpec NoMethodError: undefined method length for nil:NilClass

I'm new to Ruby and RSpec trying to write a unit case for string length. I have 3 rb files as follows
1. Calling file
require_relative 'ruby_final_operations'
require_relative 'ruby_helper'
require 'uri'
require 'open-uri'
require 'prime'
module RubyOperations
# Public: Various commands for the user to interact with RubyCommand.
class Command
res = RubyOperations::Operations.new
res.letter_count(res.inputstr)
2nd File - Method Implementation
require_relative 'ruby_helper'
require 'logger'
$FILE_LOG = RubyOperations.create_log(File.expand_path('~/RubyOperations_LOG.log'), Logger::DEBUG)
$STD_LOG = RubyOperations.create_log(nil, Logger::INFO)
module RubyOperations
class Operations
def inputstr
RubyOperations.log('Enter the String:[Length 20]',:BOTH)
#str = gets.chomp
raise StandardError if #str =~ /\d/ || #str.empty? || #str.length > 20
rescue StandardError,ArgumentError => e
RubyOperations.log(e,:ERROR)
end
def letter_count(str)
result = #str.length
RubyOperations.log("The number of letters in the string: #{result}",:BOTH)
end
3rd file - RSpec
require 'ruby_final_operations'
describe 'RubyOperations' do
describe 'Operations' do
subject = RubyOperations::Operations.new
describe '.letter_count' do
context 'when operation is provided' do
it 'returns letter count' do
allow(subject.letter_count("hello").to receive(:result).and_return(5)
end
end
end
The problem is that in the 2nd File he argument is 'str' but the typed string is stored is '#str'.
How can I pass the string 'hello' from the rspec file to test this.
There are a few issues:
Calling a instance_method with an argument that is not used
def letter_count #get rid of argument, the argument does nothing,
#basically it looks you added the argument,
# just, so you can call the other method there.
Make your main simple, with a clear sequence
res.inputstr
res.letter_count
But about your actual question, in your test you change the wrong thing the wrong method
allow(subject.letter_count("hello").to receive(:result).and_return(5)
# letter count should do the log entry, not return five, at least that what your method say
So you probably want to set the #str before you test the letter_count method.
subject.instance_variable_set("hello")
# then test for what you expect the method to return
expect(subject.letter_count).to eq(5)
# this specific test will fail, because you are doing a log entry, and not return the length on letter_count.

Minitest - Tests Don't Run - No Rails

I'm just starting a small project to emulate a Carnival's ticket sales booth and one of the guidelines was to test that a user can enter the number of tickets. The program runs in the console and I eventually (hopefully) figured it out how to implement this test thanks to #Stefan's answer on this question.
The problem is that now, when I run the test file, minitest says:
0 runs, 0 assertions, 0 failures, 0 errors, 0 skips
I get the same result when I try to run the test by name using ruby path/to/test/file.rb --name method-name. I'm not sure if this is because my code is still faulty of if it's because I've set up minitest incorrectly. I've tried to look up similar problems on SO but most questions seem to involve using minitest with rails and I just have a plain ruby project.
Here's my test file:
gem 'minitest', '>= 5.0.0'
require 'minitest/spec'
require 'minitest/autorun'
require_relative 'carnival'
class CarnivalTest < MiniTest::Test
def sample
assert_equal(1, 1)
end
def user_can_enter_number_of_tickets
with_stdin do |user|
user.puts "2"
assert_equal(Carnival.new.get_value, "2")
end
end
def with_stdin
stdin = $stdin # global var to remember $stdin
$stdin, write = IO.pipe # assign 'read end' of pipe to $stdin
yield write # pass 'write end' to block
ensure
write.close # close pipe
$stdin = stdin # restore $stdin
end
end
In a file called carnival.rb in the same folder as my test file I have
Class Carnival
def get_value
gets.chomp
end
end
If anyone can help figure out why the test is not running I'd be grateful!
By convention, tests in Minitest are public instance methods that start with test_, so the original test has no actual test methods. You need to update your test class so that the methods with assertions follow the convention as:
class CarnivalTest < Minitest::Test
def test_sample
assert_equal(1, 1)
end
def test_user_can_enter_number_of_tickets
with_stdin do |user|
user.puts "2"
assert_equal(Carnival.new.get_value, "2")
end
end
# snip...
end
Yeah always start all your tests with test_ so it knows that you want to that function/method
class CarnivalTest < MiniTest::Test
def test_sample
assert_equal(1, 1)
end
def test_user_can_enter_number_of_tickets
with_stdin do |user|
user.puts "2"
assert_equal(Carnival.new.get_value, "2")
end
end
and that should work for you

rspec expectations on block

I have the below code under test:
class MethodCache
##methods=Hash.new
def self.add_method(name, &block)
##methods[name]=block
end
def self.get_method(name)
##methods[name]
end
end
Now my spec looks like this:
describe MethodCache do
subject {MethodCache}
foo_block = ->{ puts "foo"}
it ".get_method" do
subject.add_method "foo", &foo_block
# does not work
# expect(subject.get_method("foo").to be &foo_block
# should syntax works
subject.get_method("foo").should be foo_block
end
end
I am trying to stay away from should syntax and use the expect syntax of RSpec. However it does not work in this case.
expect(subject.get_method("foo").to be &foo_block fails saying wrong number of arguments. I guess this is because the expectation block is treated as a block argument.
expect(subject.get_method("foo").to be foo_block (without the '&') does not work either. It says, the matcher expects a value and not argument.
What am I missing here?

Embed RSpec test in a Ruby class

I often build little single-purpose Ruby scripts like this:
#!/usr/bin/env ruby
class Widget
def end_data
DATA.read
end
def render_data source_data
source_data.upcase
end
end
w = Widget.new
puts w.render_data(w.end_data)
__END__
data set to work on.
I'd like to include RSpec tests directly inside the file while I'm working on it. Something like this (which doesn't work but illustrates what I'm trying to do):
#!/usr/bin/env ruby
class Widget
def end_data
DATA.read
end
def render_data source_data
source_data.upcase
end
def self_test
# This doesn't work but shows what I'm trying to
# accomplish. The goal is to have RSpec run these type
# of test when self_test is called.
describe "Widget" do
it "should render data properly" do
#w = Widget.new
expect(#w.render_data('test string')).to eq 'TEST STRING'
end
end
end
end
w = Widget.new
w.self_test
__END__
data set to work on.
I understand this is not the normal way to work with RSpec and isn't appropriate in most cases. That said, there are times when it would be nice. So, I'd like to know, is it possible?
There are two things. First off rspec by default won't pollute the global namespace with methods like describe and so on. The second thing is that you need to tell rspec to run the specs after they've been declared.
If you change your self_test method to be
RSpec.describe "Widget" do
it "should render data properly" do
#w = Widget.new
expect(#w.render_data('test string')).to eq 'TEST STRING'
end
end
RSpec::Core::Runner.invoke
(having of course done require 'rspec' then that will run your specs).
The invoke methods exits the process after running the specs. If you don't want to do that, or need more control over where output goes etc. you might want to drop down to the run method which allows you to control these things.

Test output to command line with RSpec

I want to do is run ruby sayhello.rb on the command line, then receive Hello from Rspec.
I've got that with this:
class Hello
def speak
puts 'Hello from RSpec'
end
end
hi = Hello.new #brings my object into existence
hi.speak
Now I want to write a test in rspec to check that the command line output is in fact "Hello from RSpec"
and not "I like Unix"
NOT WORKING. I currently have this in my sayhello_spec.rb file
require_relative 'sayhello.rb' #points to file so I can 'see' it
describe "sayhello.rb" do
it "should say 'Hello from Rspec' when ran" do
STDOUT.should_receive(:puts).with('Hello from RSpec')
end
end
Can someone point me in the right direction please?
Here's a pretty good way to do this. Copied from the hirb test_helper source:
def capture_stdout(&block)
original_stdout = $stdout
$stdout = fake = StringIO.new
begin
yield
ensure
$stdout = original_stdout
end
fake.string
end
Use like this:
output = capture_stdout { Hello.new.speak }
output.should == "Hello from RSpec\n"
The quietly command is probably what you want (cooked into ActiveSupport, see docs at api.rubyonrails.org). This snippet of RSpec code below shows how to ensure there is no output on stderr while simultaneously silencing stdout.
quietly do # silence everything
commands.each do |c|
content = capture(:stderr) { # capture anything sent to :stderr
MyGem::Cli.start(c)
}
expect(content).to be_empty, "#{c.inspect} had output on stderr: #{content}"
end
end
So you don't have to change your main ruby code I just found out you can do something like this:
def my_meth
print 'Entering my method'
p 5 * 50
puts 'Second inside message'
end
describe '#my_meth' do
it 'puts a 2nd message to the console' do
expect{my_meth}.to output(/Second inside message/).to_stdout
end
end
When checking for a desired output text I used it inside / / like a Regexp because after many many maaany tests and looking around, the STDOUT is everything that is outputted so I found it to be better to use Regex so you could check the whole STDOUT for the exact text that you want.
Like I put it, it works in the terminal just perfect.
//Just had to share this, it took me days to figure it out.
it "should say 'Hello from Rspec' when run" do
output = `ruby sayhello.rb`
output.should == 'Hello from RSpec'
end

Resources