Add capture_io method from Minitest #399

Closed
wants to merge 4 commits into
from

Conversation

Projects
None yet
4 participants

I was writing a builder with thor and wanted to test my task with proper output. What I needed is to capture $stdout and $stdin to test those things. I searched the web for it and could only find capture_io frm Minitest.

The main idea of capture_io is written in the following snippet:

require 'stringio'

def capture_io
  captured_stdout, captured_stderr = StringIO.new, StringIO.new

  orig_stdout, orig_stderr = $stdout, $stderr
  $stdout, $stderr         = captured_stdout, captured_stderr

  yield

  return captured_stdout.string, captured_stderr.string

ensure
  $stdout = orig_stdout
  $stderr = orig_stderr
end

out, err = capture_io do
  puts "Some info"
  warn "You did a bad thing"
end

puts out

It is really just a helper method and @myronmarston mentioned on twitter the following:

@wikimatze It does not, but I'm open to adding a matcher for it. Want to open an issue?

— Myron Marston (@myronmarston) December 23, 2013
<script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>

I tried to implement it as a matcher but lowered the code coverage. I'm not sure what and how to test it, help is more than welcome.

If anybody wants to use the ability before it's included in RSpec you can make use of by adding the following lines to your spec_helper.rb:

require 'minitest/unit'

RSpec.configure do |conf|
  conf.include Minitest::Assertions
end

If this pull request is excepted, I can cleanup my helper and stay in the RSpec environment.

Happy Xmas.

@myronmarston myronmarston and 1 other commented on an outdated diff Dec 24, 2013

lib/rspec/matchers/built_in/be_instance_of.rb
@@ -9,6 +9,21 @@ def match(expected, actual)
def description
"be an instance of #{expected}"
end
+
+ def capture_io
+ captured_stdout, captured_stderr = StringIO.new, StringIO.new
+
+ orig_stdout, orig_stderr = $stdout, $stderr
+ $stdout, $stderr = captured_stdout, captured_stderr
+
+ yield
+
+ return captured_stdout.string, captured_stderr.string
+
+ ensure
+ $stdout = orig_stdout
+ $stderr = orig_stderr
+ end
@myronmarston

myronmarston Dec 24, 2013

Owner

Why has this been added to be_instance_of?

@wikimatze

wikimatze Dec 25, 2013

My mistake, fixed with this commit.

@myronmarston myronmarston and 1 other commented on an outdated diff Dec 24, 2013

lib/rspec/matchers.rb
@@ -252,6 +252,12 @@ def be_within(delta)
BuiltIn::BeWithin.new(delta)
end
+ # Passes if <tt>actual.equal?(expected)</tt> (object identity).
+ #
+ def capture_io(expected)
+ BuiltIn::CaptureIo.new(expected)
+ end
@myronmarston

myronmarston Dec 24, 2013

Owner

capture_io sounds more like a helper method than a matcher, and rspec-expectations is generally not it the business of providing helper methods....I'm not sure it fits.

What I think does fit is a couple matchers:

expect { do_something }.to output_to_stdout("some string")
expect { do_something }.to output_to_stderr("some string")
@lucapette

lucapette Dec 24, 2013

Contributor

👍 I wasn't sure about the naming and your suggestion sounds good to me!

Owner

myronmarston commented Dec 24, 2013

As far as tests go, here's a few to get you started:

shared_examples_for "output_to_stream" do |stream_name|
  matcher_method = :"output_to_#{stream_name}"

  define_method :matcher do |*args|
    send(matcher_method, *args)
  end

  context "expect { ... }.to #{matcher_method} (with no arg)" do
    it "passes if the block prints to #{stream_name}" do
      expect { stream.puts 'foo' }.to matcher
    end

    it "fails if the block does not print to #{stream_name}" do
      expect {
        expect { }.to matcher
      }.to fail_with("expected block to output to #{stream_name}, but did not")
    end
  end

  context "expect { ... }.not_to #{matcher_method} (with no arg)" do
    it "passes if the block does not print to #{stream_name}" do
      expect { }.not_to matcher
    end

    it "fails if the block prints to #{stream_name}" do
      expect {
        expect { stream.puts 'foo' }.not_to matcher
      }.to fail_with("expected block to output to #{stream_name}, but did not")
    end
  end

  context "expect { ... }.to #{matcher_method}('string')" do
    it "passes if the block prints that string to #{stream_name}" do
      expect { stream.puts 'foo' }.to matcher('foo')
    end

    it "fails if the block does not print to #{stream_name}" do
      expect {
        expect { }.to matcher('foo')
      }.to fail_with('expected block to output "foo" to #{stream_name}, but output nothing')
    end

    it "fails if the block prints a different string to #{stream_name}" do
      expect {
        expect { stream.puts 'food' }.to matcher('foo')
      }.to fail_with('expected block to output "foo" to #{stream_name}, but output "food"')
    end
  end
end

describe "output_to_stdout matcher" do
  include_examples "output_to_stream", :stdout do
    let(:stream) { $stdout }
  end
end

describe "output_to_stderr matcher" do
  include_examples "output_to_stream", :stderr do
    let(:stream) { $stderr }
  end
end

There's a lot more needed to flesh out all the different cases but that should get you started.

Zorbash commented Dec 30, 2013

Looking forward to see this one get merged (with proper tests of course)

@myronmarston myronmarston commented on the diff Dec 30, 2013

lib/rspec/matchers/built_in/couple.rb
@@ -0,0 +1,45 @@
+require 'stringio'
+
+module RSpec
+ module Matchers
+ module BuiltIn
+ class Couple < BaseMatcher
@myronmarston

myronmarston Dec 30, 2013

Owner

Why did you call this Couple? I have no idea what that has to do with printing to a stream...

@myronmarston myronmarston commented on the diff Dec 30, 2013

lib/rspec/matchers/built_in/couple.rb
+ orig_stdout, orig_stderr = $stdout, $stderr
+ $stdout, $stderr = captured_stdout, captured_stderr
+
+ yield
+
+ return captured_stdout.string, captured_stderr.string
+
+ ensure
+ $stdout = orig_stdout
+ $stderr = orig_stderr
+ end
+ end
+
+ def output_to_stdout(expected)
+ Couple.new(expected)
+ end
@myronmarston

myronmarston Dec 30, 2013

Owner

Why is this method here? You've already defined this in lib/rspec/matchers.rb.

@myronmarston myronmarston commented on the diff Dec 30, 2013

lib/rspec/matchers/built_in/couple.rb
+
+ return captured_stdout.string, captured_stderr.string
+
+ ensure
+ $stdout = orig_stdout
+ $stderr = orig_stderr
+ end
+ end
+
+ def output_to_stdout(expected)
+ Couple.new(expected)
+ end
+
+ def output_to_stderr(expected)
+ Couple.new(expected)
+ end
@myronmarston

myronmarston Dec 30, 2013

Owner

Why is this method here? You've already defined this in lib/rspec/matchers.rb.

@myronmarston myronmarston commented on the diff Dec 30, 2013

lib/rspec/matchers/built_in/couple.rb
@@ -0,0 +1,45 @@
+require 'stringio'
+
+module RSpec
+ module Matchers
+ module BuiltIn
+ class Couple < BaseMatcher
+ def initialize(expected)
+ @expected = expected
+ end
+
+ def matches?
+ output_to_stream
@myronmarston

myronmarston Dec 30, 2013

Owner

This needs to compare what is output (if anything) against @expected.

Owner

myronmarston commented Dec 30, 2013

@matthias-guenther -- there's a fair bit here left to do. I wasn't sure if you were ready for more feedback, but I left a few comments. Let us know if you get to a point of not being able to finish this (e.g. due to getting too busy with other things, or whatever).

@myronmarston the next month will be very busy for me and I already spent the maxium amount of time to this issue, I'm now at a point where I need a remote pairing session to solve this task.

So you or @Zorbash are going to write the tests for it, I'm eager to review and learn from it, thanks for the feedback and comments so far.

Owner

myronmarston commented Jan 9, 2014

I'm going to close this in favor of #410 as that's further along.

Owner

myronmarston commented Jan 9, 2014

Thanks for getting the ball rolling on this, @matthias-guenther!

That's fine, next time when I know that I will have more time, I will solve the problem on my own. I'm sure there are some missing features which needs to be added to RSpec.

Contributor

lucapette commented Jan 9, 2014

@matthias-guenther yes, thank you for the inspiration. @myronmarston I have limited about of time this week but I'm almost done with the tasks coming out of your great feedback.

Zorbash commented Jan 9, 2014

Great! I wish i'd seen the comment by @matthias-guenther earlier, so that i'd helped more.

No problem @Zorbash!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment