Unitテストとは?から学ぶ、RubyでUnitテストをする一連の手順

Rubyで書いたジャンケンのコードを`Unitテストする方法を調べたのでメモします。

Unitテストとは

Unitテストはざっくり、ばっさり言うと関数やメソッドといった最小の機能単位で、期待した動作が得られるかどうかを確認することらしい。システム全体として正しく動いているかをテストするのはテストフェーズになるが、Unitテストはテストフェーズまで待たずに、プログラマがソースを書いた時に、一緒にやってしまうのが主流とのこと。プログラマ大変^^;

Unitテストはプログラマの負荷が倍増する恐ろしいものだけど、最終的にはUnitテストをきっちりしておくと、手戻りなどが無くなってプログラマ本人が特をするそう。

参考文献

まとめ:

親父の小言と冷たい酒、Unitテストは、今は効かぬが後で効く。

RubyバンドルのテストUnitの歴史

歴史を調べるつもりはなかったが、どんなテスト用フレームワークがあるか調べていたら歴史に行き着きました。こんな感じのRuby用Testing frameworkの歴史みたいです。

Testing framework 歴史(ざっくり)
RubyUnit
Lapidary
RubyUnit と Lapidaryというtesting frameworkがRuby用のたぶん最初のもの
Test::Unit 上記の2つが統合してTest::Unitになる。これがRuby1.8からバンドルされ、名前をtest/unit, test-unitなどに変えながらも標準testing frameworkの座を守る。
minitest Test::Unitの複雑さに異を唱える人が表れる。なんとTest::Unitのメンテナー。もっと軽くて小さくて管理のし易いものにしたいと思い、miniunit(後のminitest)を生み出す。Ruby 1.9.1から現在に至るRuby標準バンドルTesting frameworkになる。
RSpec RubyのバンドルTesting frameworkの歴史には全く登場しないものの、Railsの超有名チュートリアルサイトで取り上げられるなど、幅広い支持を得ている。

ということで、今回のテストでは標準バンドルされているminitestを使用することにします。

参考文献

minitestの使い方

とにかくminitestを動かす

ここを参考に、そもそもバンドルのminitestが動作するかを確認

http://allabout.co.jp/gm/gc/452071/

上記サイトでは最初にGemを使ってminitestを入れているが、この手順は省略。バンドルのminitestで動作するか確認。

test_janken.rb

にすると動いた。以下が参考になった。

https://github.com/seattlerb/minitest/issues/280

実行結果はこちら。1つのテストで予想値を得られたことが確認できた。

外部の–.rbファイルをテストコードに読み込んでunittestする

本番の開発を想定すると、システム開発の本番ファイルとテストコードは、別々の.rbファイルになる。なので、テストコードに外部の本番コードを読み込んでテストする。

本番コード(外部ファイル)をjanken.rbとし、テストコードと同じディレクトリにあるとすると、

の書式で外部ファイルを読み込める。参考サイトによっては、

と書いてあるが、これだとRuby 2.1.4ではエラーが出たので、以下のサイトを参考に修正した。

http://blog.ruedap.com/2011/05/31/ruby-require-load-path

完成形は以下のとおり。

本番プログラム(ジャンケンするプログラム)のテストで使ってみる

本番プログラムのソース

まず、ジャンケンプログラムのコード

main.rb

janken.rb

このプログラムにテストをしていく。

標準入力取得部分のUnitテスト

以下の様な標準入力を取得して、インスタンス変数に格納する部分をテストする。

これがいきなり難しい、前述のサンプルのassert-equalとかではテストできなさそう。まず、標準入力をテストプログラムから与える部分をどうやって書けばいいかが分からない。どうやら「スタブ」や「モック」というテクを使うらしい。

参考

http://d.hatena.ne.jp/saliy1/20100724/1279980413

まずはget_player_handに直接STDINだと、テストの入力値を渡せないので、以下のように、オブジェクト生成時に入力値を渡せるように変更した。もちろん、入力値を渡してくるのはテストプログラム。

あとは、テストプログラムの中でJanken.newするときに、StringIOというオブジェクトを渡す。

このStringIOは文字列をIOオブジェクト(標準入力STDINや出力STDOUTなどで使うオブジェクト)のように扱うことができるオブジェクト。

コレを使うと @in << “g” というように、あとから入力値を渡せる。つまり、ジャンケンプログラムを実行した後に、標準入力から”g”という文字を打ちこむのと同様の状況をシミュレートしてくれる。

IOオブジェクト.rewindというのは、ファイルポインタを先頭に移動するメソッド。

これでassert_equal “g”, @janken.get_player_handを実行すると、標準入力のテスト項目をクリアできた。

case分で複数種類の返り値が得られる場合のUnitテスト

標準入力以外の部分のテストプログラムの方法は、ググった情報で十分対応して行けそう。ジャンケンプログラムのソースはcase文で値を返すメソッドが多いので、返り値が想定範囲内であることをテストするassert_includesを使えば良さそう。これでコンピュータのジャンケンの手をランダム生成するメソッドのテストを以下のように書いてみた。

返り値はランダムだが、数学的には33.3…%なので、偏りも考慮して10回テストすることにした。

次にプレイヤーとコンピュータの手から勝敗を決定するロジック部分のテストをする。が、以下のようにjudge メソッドの中にプレイヤーとコンピュータの手を取得していたために、テストがしづらくなっていることに気づいた。

テストプログラムからプレイヤーとコンピュータの手を入力値として与え、正しい勝敗結果が出力されるかをテストしたいが、入力値がjudgeメソッドの中で固定になっている。

テストを考えて、judgeメソッドを以下のように修正した。judgeメソッドの引数として、プレイヤーとコンピュータの手を与えられるようにした。引数を何も与えなければ、今までどおりインスタンス変数のプレイヤーとコンピュータの手を取得する。

一つ注意点はreturn @resultを末尾に加えたこと。Rubyは最終行が返り値になるとのことだったので、returnを書かなくてもcase文内の条件適合行(つまり@result)が返り値になると思っていたが、returnで明示的に@resultしないとテストプログラムで得られるjudgeメソッドの返り値はnilになった。前述のコンピュータの手のランダム生成メソッドget_com_handでは同じcase文の記述で返り値が得られたのに…何でだろう??

judgeメソッドから省いた、クラス内の各メソッドを呼び出すmain的な処理は、resultメソッドに全てまとめた。resultメソッドはメソッドの呼び出しのみを行うメソッド。つまり、システム全体の処理を行っているので、Unitテストの対象にはならない。と思う。たぶん。

最終的なjudgeのテストプログラムは以下のように書いた。全9通りをテスト。

画面出力クラスのUnitテスト

Unitテストを意識するとクラスやメソッドの作りの悪さが分かってくる。画面出力クラスJanken_printは当初以下の通り。

これをUnitテストするときに気づいたが、initializeでわざわざプレイヤー、コンピュータの手、勝敗結果をインスタンス変数化しているが、このインスタンス変数を使用するのはJanken_printクラスにある3つのメソッドのうちdisp_resultの1つのみ。なので、インスタンス変数化した意味が無い。

また、この仕様のせいで、Janken_printクラスをテストする場合、入力値(ジャンケンの手)を変更するためにはJanken_printクラスを何回もオブジェクト化しなければいけない。

ということで、以下のように、プレイヤー、コンピュータの手、勝敗結果はdisp_resultメソッドの引数として、渡すように変更した。

これにともない、Jankenクラスのresultメソッドも以下のように変更

上記の対応で、ぐっとUnitテストがやりやすくなった。ジャンケンの手と勝敗結果の画面出力文字列への変換メソッドhand_to_stringとresult_to_stringは以下のようにサクッと書けた。

ただ、最後の最終結果の画面出力disp_resultメソッドのUnitテストで問題が。以下のようにメソッド内でprintするとreturn値が得られないのでUnitテストができなかった。

なので、以下のようにdisp_resultメソッドでは最終結果の文字列をreturnして、このメソッドを呼び出す側でputsして画面出力するほうが良い。これからはテストを意識しながら本番コードも作り始めたほうが良さそうだ。(そう開発手法をTDDというらしい)

という感じで、最終的なテストコード、ジャンケンプログラムは以下のようになった。テストを通してジャンケンプログラム自体も洗練されてきた気がする。

main.rb

janken.rb

test_janken.rb

その他に覚えたhacks

  • %w[foo bar baz]と書くと[“foo”, “bar”, “baz”]の配列が得られる。配列リテラルと言うテクらしい。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です