標準入出力をStringIOに置き換える

helloコマンドは標準入力を表示します。オプションtmp_optionは特に使われていません。

require 'thor'
require_relative '../lib/my_app'

class Command < Thor
  desc 'hello', 'command for test'
  option :tmp_option, :aliases =>'-t', :type => :string
  def hello
    puts($stdin.eof? ? "no pipe" : $stdin.read)
  end
end

この標準入力をターミナル上ではなく、テストコードから入力します。全く同じには出来ないのですが、同じような動作をするStringIOをモックとして使います。StringIOStringIO、つまり入出力のメソッドがついたものです。

putsなどの出力は、StringIOをバッファとして溜め込まれます。そしてバッファから内容を出力してテストをするわけです。標準入力はグローバル変数$stdinに入っていますので、レシーバに$stdinを指定しているなら、これにStringIOを直接入れてもいいかもしれません。StringIOの詳細な動きは、最後にテストコードを貼っておきます。

$stdin.eof?で、標準入力がない場合を判断しています。

class HelloTest < Test::Unit::TestCase
  test "hello" do
    $stdin = StringIO.new("hello\nworld\n")
    out = capture_output { Command.start(%w{hello -t hoge}) }[0]
    assert_equal out, "hello\nworld\n"
  end

標準入出力を切り替えられるようにする

直接$stdinに入れると、$stdinの影響がどこまであるか考える必要があります。なので、標準入出力を切り替えれるコネクタをインスタンス変数で用意して置くと、より安全になります。もちろんputsなど自分のコードで使っている同じメソッドがあるオブジェクトを使います。

class MyApp
  attr_accessor :input, :output

  def initialize
    self.input = $stdin
    self.output = $stdout
  end

  def say
    output.puts(input.gets.chomp)
  end
end
require 'thor'
require_relative '../lib/my_app'

class Command < Thor
  desc 'hello', 'command for test'
  option :tmp_option, :aliases =>'-t', :type => :string
  def hello
    puts $stdin.read
  end

  desc 'say', 'command for test'
  option :tmp_option, :aliases =>'-t', :type => :string
  def say
    app = MyApp.new
    app.input = StringIO.new(options['tmp_option'])
    app.say
  end
end
class HelloTest < Test::Unit::TestCase
  test "hello" do
    $stdin = StringIO.new("hello\nworld\n")
    out = capture_output { Command.start(%w{hello -t hoge}) }[0]
    assert_equal out, "hello\nworld\n"
  end

  test "say" do
    $stdin = StringIO.new("hello")
    out = capture_output { Command.start(%w{hello -t hoge}) }[0]
    assert_equal out, "hello\n"
  end
end

StringIOのテスト

putsなどの出力をStringIOに溜め込み、readgetsで取り出します。readは全てを、getsは一行ずつ読み込みます。getsを使うと、プロパティlinenoが進められます。注意点としては、この2つを使う前にrewindを使ってポインタを先頭に戻す必要があります。この場合のポインタとは、内容をどこから読み込むかという目印です。putsなどで出力していくとポインタは末尾にあるので、それを先頭に戻してから読み込むわけです。

require 'test/unit'
class StringIOTEST < Test::Unit::TestCase
  test "standard test" do
    io = StringIO.new
    assert_equal io.lineno, 0
    io.puts('abcd')
    io.puts('efg')
    assert_equal io.lineno, 0

    assert_equal io.read, ''
    assert_true io.eof?
    io.rewind
    assert_false io.eof?
    assert_equal io.lineno, 0
    assert_equal io.read, "abcd\nefg\n"
    assert_equal io.lineno, 0
    assert_equal io.read, ''

    io.rewind
    assert_equal io.gets, "abcd\n"
    assert_equal io.lineno, 1
    assert_equal io.gets, "efg\n"
    assert_equal io.lineno, 2
    assert_nil io.gets
    assert_equal io.lineno, 2

    io.lineno = 0
    assert_equal io.lineno, 0
    assert_nil io.gets
    assert_equal io.lineno, 0
    io.rewind
    assert_equal io.gets, "abcd\n"
    assert_equal io.lineno, 1

    io = StringIO.new("abcdefg")
    assert_equal io.read, 'abcdefg'
  end
end