putsの動作をテストする...

今日、人に説明する用にコンソールベースのコードを書いていた。
で、手動でテストするのが面倒なのでテストを書いたのですが、putsのテストが必要なことに気がついてテスト作りました。何か、putsのテストって意味あるの?という感じですが、晒しておきます。

putsのテスト

def assert_match_with_stdout(expected)
  $stdout = StringIO.new
    
  begin
    yield
    $stdout.rewind
    assert_match expected, $stdout.read
  ensure
    $stdout.close
    $stdout = STDOUT
  end
end


$stdoutの値を入れ替えて、標準出力の先を変えている。で、それを使って結果を見ている。
トランザクションの考え方で、ブロック内だけ標準出力の先を変え、抜けるときには元に戻すようにしました。
って、当たり前の実装ですが。


まぁ、ブロック内だけ有効にしたのは、$stdoutを書き換えたままテストすると、putsの結果がどんどんたまっていってしまい、結果が正しいか分からなくなるからです。
書き込まれればファイルポインタがずれるので、assertする際にはもとに戻さなければいけません。ですが、何が書き込まれたかは分からないので、seekでずれた分だけずらすのが面倒なのです。テスト前のファイルポインタの位置をとって置けば良いのですが、あまり綺麗なコードとは言えないかと。で、rewindしたのですが、すると、最初に述べた通り、前に書き込まれたテキストにマッチする可能性があり、直前にそれが書き込まれたか分からないという話なのです。


なので、ブロック内だけに制限しました。まぁ、ずっと標準出力の先を変えておくと、テストでエラーがでても変えた先に書き込まれてしまうので、このような実装になるのが自然なのかとも思います。

全ソース

エラーをputs出力しています。例外は今回説明する部分の話でなかったので、このような実装になりました。
ただの、アドレス帳のシミュレートです。

  • address_book.rb
class User
  attr_reader :name, :address
  
  def self.validate_presence_of?(user)
    (user.name != nil) && (user.address != nil)
  end
    
  def initialize(name, address)
    @name, @address = name, address
  end

  def valid?
    self.class.validate_presence_of?(self)
  end
end

class AddressBook
  attr_reader :users
  
  def self.failure_notify(type)
    msg = 
      case type
      when 'invalid user'
        '不正なユーザーデータのため登録できませんでした'
      when 'duplicate name'
        'その名前は既に登録されています'
      when 'duplicate address'
        'そのアドレスは既に登録されています'
      else
        raise 
      end
    
    puts msg
    false
  end
  
  def find(obj, range = nil)
    obj = obj.name if obj.respond_to?(:name)
    
    if range == "address" 
      @users.find_all{|user| user.address == obj}
    else
      @users.find_all{|user| user.name == obj}
    end
  end
  
  def initialize(user = nil)
    @users = []

    if user
      if user.valid?
        @users << user
      else
        self.class.failure_notify('invalid user')
      end
    end
    
    self
  end
  
  def add(user)
    if addable_user?(user)
      @users << user
      self
    else
      false
    end
  end
  
  def size
    @users.size
  end
  
  private
  
  def addable_user?(user)
    case 
    when !user.valid?
      self.class.failure_notify('invalid user')
    when exist_name?(user.name)
      self.class.failure_notify('duplicate name')
    when exist_address?(user.address)
      self.class.failure_notify('duplicate address')
    else
      true
    end
  end
  
  def exist_name?(name)
    !find(name).empty?
  end
  
  def exist_address?(address)
    !find(address, 'address').empty?
  end
end


テストは次の通り。

  • address_book_test.rb
require 'test/unit'
require File.dirname(__FILE__) + '/address_book'
require 'stringio'

class UserTest < Test::Unit::TestCase
  def setup
    @bob   = User.new('bob', 'bob@gmail.com')
  end
  
  def test_record
    assert_equal 'bob',   @bob.name
    assert_equal 'bob@gmail.com', @bob.address
  end
  
  def test_valid
    assert @bob.valid?
    invalid_params = [[nil, nil],
                      ['hoge', nil],
                      [nil, 'hoge']]

    invalid_params.each do |p|
      assert !User.new(*p).valid?, p.inspect
    end
  end
end

class AddressBookTest < Test::Unit::TestCase
  def setup
    @bob   = User.new('bob', 'bob@gmail.com')
    @becky = User.new('becky', 'becky@gmail.com')
    @address_book = AddressBook.new
  end
    
  def test_initialize
    assert_equal 0, @address_book.size

    ab = AddressBook.new(@bob)
    assert_equal 1, ab.size
    assert_equal [@bob], ab.users
  
    assert_match_with_stdout('不正') do 
      AddressBook.new(User.new(nil, nil))
    end
  end
  
  def test_size
    assert_equal 0, @address_book.size
    assert_equal 1, AddressBook.new(@bob).size
  end

  def test_find
    assert_equal [], @address_book.find(@bob.name)
    assert_equal [], @address_book.find(@bob.address, 'address')
    
    @address_book.add(@bob)
    assert_equal [@bob], @address_book.find(@bob.name)
    assert_equal [@bob], @address_book.find(@bob.address, 'address')
    assert_equal [],     @address_book.find(@bob.address)
  end
  
  def test_add
    assert_equal 0, @address_book.size
    assert_equal [], @address_book.find(@bob)
    @address_book.add(@bob)
    assert_equal 1, @address_book.size
    assert_equal [@bob], @address_book.find(@bob)
    @address_book.add(@becky)
    assert_equal 2, @address_book.size
    
    assert_match_with_stdout('不正') do 
      @address_book.add(User.new(nil, nil))
    end
    assert_match_with_stdout('名前') do
      @address_book.add(User.new(@bob.name, 'hoge'))
    end
    assert_match_with_stdout('アドレス') do 
      @address_book.add(User.new('hoge', @bob.address))
    end
  end
  
  def assert_match_with_stdout(expected)
    $stdout = StringIO.new
    
    begin
      yield
      $stdout.rewind
      assert_match expected, $stdout.read
    ensure
      $stdout.close
      $stdout = STDOUT
    end
  end
end