ククログ

株式会社クリアコード > ククログ > RubyKaigi 2015:The history of testing framework in Ruby #rubykaigi

RubyKaigi 2015:The history of testing framework in Ruby #rubykaigi

RubyKaigi 2015の2日目(2015年12月12日)にThe history of testing framework in RubyというタイトルでRubyのテスティングフレームワークの歴史を紹介しました。

関連リンク:

内容

この発表の内容は次の2つにわかれています。

  1. Rubyにバンドルされているテスティングフレームワークを中心に、Ruby用のテスティングフレームワークの歴史を紹介

  2. Test::Unit APIの特徴を紹介

歴史はRubyのテスティングフレームワークの歴史(2014年版)をベースにそろそろリリースされるRuby 2.3までの情報を追加したものになっています。RubyUnitについてもっと知りたい方はRubyUnitの開発者である助田さんが書いた[Ruby] RubyKaigi 2015(2日目): 遠回りするかなも読むとよいです。

Test::Unit APIについてはスライドの72ページ以降を参照してください。ざっくりというとTest::Unit APIは普通のRubyのコードを書くようにテストを書けることを大事にしている、ということを具体例を交えて説明しています。

ただし、フィクスチャーのAPIはそれほどRubyらしくありません。いつものRubyでは次のようにブロックを使って後処理の詳細を隠蔽できます。

File.open("README") do |file|
  # ...
end # ← ブロックが終わったら開いたファイルを確実に閉じる

しかし、Test::Unit APIでは次のように別のメソッドで後処理を書かなければいけません。いつものRubyの書き方をできていないということです。

def setup
  @file = File.open("README")
end

def teardown
  @file.close
end

この点についてFastly, Travis CI, GitHub 共催 RubyKaigi 前々夜祭田中 哲さんから指摘がありました。そこで、発表までの間にtest-unit(発表を聞いた人なら区別をつけられるでしょうがgemのやつです)のmasterでは次のように使えるようにしました。

def setup
  File.open("README") do |file|
    @file = file
    yield # ここでtest_readが動く
  end # テストが終わったらファイルを閉じる
end

def test_read
  assert_equal("XXX", @file.read)
end

ただ、@file = fileの部分がもやっとします。ふだんのRubyのコードではインスタンス変数に設定する使い方をしないからです。Test::Unit APIではインスタンス変数にテストで使うオブジェクトを設定するのは普通なので、テストコードという文脈では普通ですが、ふだんのRubyのコードという文脈では不自然なのです。

別の案として次のようなAPIも検討しましたが、やはりインスタンス変数に設定するところがもやっとします。(この書き方は既存のtest-unitですでに使えますが、非公開のAPIを使うことになるので実験用のコード以外では使わないでください。)

def run_test
  File.open("README") do |file|
    @file = file
    super
  end
end

なお、環境変数を設定するような次のケースではsetupの中でyieldする書き方で違和感はありません。インスタンス変数を使わないからです。

def setup
  original_lang = ENV["LANG"]
  begin
    ENV["LANG"] = "C"
    yield
  ensure
    ENV["LANG"] = original_lang
  end
end

File.openのようにブロックにデータを渡す使い方のときは、次のようにテストの中で実行した方がよいのかもしれません。

def test_read
  File.open("README") do |file|
    assert_equal("XXX", file.read)
  end
end

APIについてなにかアイディアがある方はtest-unit/test-unitのissueにコメントをお願いします。RubyKaigi 2015中には次のフィードバックをもらいました。

  • setupではなくaroundなど違う名前のAPIにした方がよいのではないか

  • そもそもなぜsetupなのか。テスト毎にインスタンスを作るならinitializeでやるのが普通ではないか

  • テストメソッド(この例ではtest_read)が引数を受け取るようにしたらよいのではないか(そうすればインスタンス変数に設定する必要はなくなる)

  • yieldにオブジェクトを渡したら自動でインスタンス変数に設定するのはどうか

違う名前のAPIはよいかもしれませんが、aroundはなじまないのが悩ましいところです。Test::Unitの既存のフィクスチャーのAPIはsetup/teardownというように役割を名前にしているのでaroundというように順序を名前にするのはなじまないのです。RSpecのようにbefore/afterというように順序を名前にしているならなじむのかもしれません。

なお、setupの中で後処理も実行されることがもやっとするのでsetupではない名前がよさそう、ということであれば、そこは心配しなくてもよいです。File.openのように本処理を名前にしているメソッドがブロック付きで呼ばれたら後処理をするようになることは普通のRubyのコードだからです。setupも本処理の名前です。

initializeでやるのはよいかもしれませんが、後処理をするタイミングを作れないのが悩ましいところです。普通のRubyのコードではFile.open {...}のように処理の範囲を明示しないときはGCのタイミングで後処理が動きます。テストでは他のテストに影響を残さないように、テストが終わったら確実に後処理を実行しておきたいです。

テストメソッドが引数を受け取ると複数のセットアップ処理を指定したときに破綻しそうな点が悩ましいところです。

yieldにオブジェクトを渡すと自動でインスタンス変数を設定するのは可読性が悪くなりそうなところが悩ましいところです。

ちなみに、setupyieldしても動くようにする仕組みは次の擬似コードのように実装しています。

def run_test
  block_is_called = false
  setup do
    block_is_called = true
    test_read
  end
  test_read unless block_is_called
end

他の発表

先日RubyKaigi 2015にスピーカー・スポンサーとして参加予定でオススメした通り、咳さんActor, Thread and meが非常に興味深かったです。

bartender(咳フリークはこの名前を聞くとDivを思い出すでしょう)はRubyWorld Conference 2014で話した同期っぽいAPI実装と同じようなものでした。いまだに着手していませんが、これをベースに複数の通信を並行して進めるときにそれっぽく書けそうなAPIがあるとよさそうと検討していたのでした。

まとめ

クリアコードがシルバースポンサーとして応援したイベント「RubyKaigi 2015」での発表「The history of testing framework in Ruby」の内容を簡単に紹介しました。また、オススメの咳さんの発表についても少し触れました。

今回、スポンサーとして参加した成果はまだわかりません。配布物としてリーダブルコードワークショップのチラシとGroonga族のステッカーを置いておきました。参加者が帰った後に残った分を回収しましたが、どちらもほぼなくなっていました。興味があった方はぜひお問い合わせください。