0

I'm experimenting with RSpec. Since I don't like mocks, I would like to emulate a console print using a StringIO object.

So, I want to test that the Logger class writes Welcome to the console. To do so, my idea was to override the puts method used inside Logger from within the spec file, so that nothing actually changes when using Logger elsewhere.

Here's some code:

describe Logger do
    Logger.class_eval do
        def puts(*args)
            ???.puts(*args)
        end
    end

    it 'says "Welcome"' do
end

Doing this way, I need to share some StringIO object (which would go where the question marks are now) between the Logger class and the test class.

I found out that when I'm inside RSpec tests, self is an instance of Class. What I thought initially was to do something like this:

Class.class_eval do
    attr_accessor :my_io
    @my_io = StringIO.new
end

and then replace ??? with Class.my_io.

When I do this, a thousand bells ring in my head telling me it's not a clean way to do this.

What can I do?

PS: I still don't get this:

a = StringIO.new
a.print('a')
a.string # => "a"
a.read # => "" ??? WHY???
a.readlines # => [] ???

Still: StringIO.new('hello').readlines # => ["hello"]

whatyouhide
  • 15,897
  • 9
  • 57
  • 71

3 Answers3

1

To respond to your last concern, StringIO simulates file behavior. When you write/print to it, the input cursor is positioned after the last thing you wrote. If you write something and want to read it back, you need to reposition yourself (e.g. with rewind, seek, etc.), per http://ruby-doc.org/stdlib-1.9.3/libdoc/stringio/rdoc/StringIO.html

In contrast, StringIO.new('hello') establishes hello as the initial contents of the string while leaving in the position at 0. In any event, the string method just returns the contents, independent of position.

Peter Alfvin
  • 28,599
  • 8
  • 68
  • 106
1

It's not clear why you have an issue with the test double mechanism in RSpec.

That said, your approach for sharing a method works, although:

  • The fact that self is an anonymous class within RSpec's describe is not really relevant
  • Instead of using an instance method of Class, you can define your own class and associated class method and "share" that instead, as in the following:

    class Foo def self.bar(arg) puts(arg) end end

    describe "Sharing stringio" do

    Foo.class_eval do def self.puts(*args) MyStringIO.my_io.print(*args) end end

    class MyStringIO @my_io = StringIO.new def self.my_io ; @my_io ; end end

    it 'says "Welcome"' do Foo.bar("Welcome") expect(MyStringIO.my_io.string).to eql "Welcome" end

    end

Peter Alfvin
  • 28,599
  • 8
  • 68
  • 106
  • I am so sorry, I didn't know that `Logger` was a Ruby module :). I meant to represent a generic class to be tested (which I unfortunately named `Logger` too). So the point was testing a generic class indeed. PS: anyway, when you create `@my_io` and reference it inside `class_eval`, doesn't that `@my_io` refer to a `Logger` class variable? (given that `self` points to `Logger`). – whatyouhide Nov 06 '13 at 22:28
  • You're right, of course. Sorry about, that. I had a disconnect between my edit window and what I was testing. ;-) Anyway, I updated the answer and took into account you didn't intend to reference Ruby's Logger. – Peter Alfvin Nov 07 '13 at 00:14
0

Logger already allows the output device to be specified on construction, so you can easily pass in your StringIO directly without having to redefine anything:

require 'logger'

describe Logger do

  let(:my_io) { StringIO.new }
  let(:log)   { Logger.new(my_io) }

  it 'says welcome' do
    log.error('Welcome')
    expect(my_io.string).to include('ERROR -- : Welcome')
  end
end

As other posters have mentioned, it's unclear whether you're intending to test Logger or some code that uses it. In the case of the latter, consider injecting the logger into the client code.

The answers to this SO question also show several ways to share a common Logger between clients.

Community
  • 1
  • 1
exbinary
  • 1,086
  • 6
  • 8
  • There was a misunderstanding, see the comment on Peter's answer. – whatyouhide Nov 06 '13 at 22:28
  • Ah, i see. I would actually suggest using the same design the stdlib Logger does: allow the concrete output device to be injected (during construction or through a setter). This makes it very easy to test (just pass in a `StringIO`) and moreover decouples your `Logger` from the actual logging target - all with very little hassle and no hacks. – exbinary Nov 07 '13 at 16:52