Steven Aguilar
Steven Aguilar

Reputation: 3049

undefined method `capture' for RSpec

I am trying to write a spec to test displaying a tic-tac-toe board. I am using the method capture but it throws an error when I run the spec. I am using capture to get the output of a method call. https://apidock.com/rails/Kernel/capture Here is my method:

  def display_board
    puts " #{grid[0]} | #{grid[1]} | #{grid[2]} "
    puts "-----------"
    puts " #{grid[3]} | #{grid[4]} | #{grid[5]} "
    puts "-----------"
    puts " #{grid[6]} | #{grid[7]} | #{grid[8]} "
  end

Here is my Rspec test:

context "#display_board" do
     output = capture(:stdout) { board.display_board}
     rows = output.split("\n")
     binding.pry
     expect(rows[0]).to eq("   |   |   ")
     expect(rows[1]).to eq("-----------")
     expect(rows[2]).to eq("   |   |   ")
     expect(rows[3]).to eq("-----------")
     expect(rows[4]).to eq("   |   |   ")
  end

Upvotes: 2

Views: 1116

Answers (2)

Peter H. Boling
Peter H. Boling

Reputation: 583

capture was removed from Rails, but I liked it and brought it back, mostly for use in my spec suites. I extracted the exact Rails code, along with the complete test suite, and then improved the test suite. The gem is called silent_stream, it works with Rails 5, 6, and partially with rails 7. You can find it here: https://github.com/pboling/silent_stream

This is how you use it for output capturing:

# Make the methods avaialble:
RSpec.configure do |conf|
  conf.include SilentStream
end

# Then add an expectation on output:
it 'has output' do
  output = capture(:stdout) { subject.request(:get, '/success') }
  logs = [
      'INFO -- request: GET https://api.example.com/success',
      'INFO -- response: Status 200'
  ]
  expect(output).to include(*logs)
end

An Even Better Solution

Recent versions of RSpec have an output matcher built-in! Use this instead.

https://www.rubydoc.info/gems/rspec-expectations/RSpec%2FMatchers:output

Upvotes: 1

struthersneil
struthersneil

Reputation: 2750

The problem is that #capture was deprecated and removed.

Personally, I would separate I/O from the generation of the game content, because it's easier to work with and test that way. So instead of puts, just build up a string containing your board (with a \n at the end of each line) and test that for correctness. In the actual game, output the board to the console with one puts call.

If you really want to capture STDOUT, there is a way. $stdout is a global containing a reference to the current STDOUT stream. By default this is always set to STDOUT, a constant representing the actual STDOUT of the program, but you can redirect $stdout to another IO stream, for example a StringIO object. puts will write to whatever $stdout refers to instead of directly writing to STDOUT (there is a subtle difference there!).

$stdout = StringIO.new
puts 'this is my output'
captured_output = $stdout.string
$stdout = STDOUT # restore it when you're done

Now you have 'this is my output' (followed by a newline character) captured in captured_output. But you'll also have anything else that went to STDOUT in that time, so it's not ideal (e.g. console output in your ruby console). You should wrap that in a method that ensures it's always restored, e.g.

def capture() 
  $stdout = StringIO.new
  yield
  $stdout.string
ensure
  $stdout = STDOUT
end

You can use this with a block, e.g.

capture { puts 'xyz' } 
=> 'xyz\n'

(But you'll have an easier life if you forget this approach and just build your board up as a string.)

Upvotes: 2

Related Questions