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