イテレータのテストコード。

久し振りに、仕事絡みで Ruby のコードを書く機会がやってきました。

これまでにも仕事絡みで Ruby のコードを書いたことはあるのですが、要求開発のためのプロトタイプ (要求開発に於けるプロトタイピング) だったり、雑務のツールだったりで、余り厳密にテストコードを書いていませんでした。コード自体も簡単なものばかりです。

今回も簡単なコードではありますが、複数メンバに共通的な位置付けのコードとなるため、多少なりともシビアにテストコードを起こすことにしました。

久し振りということで、新鮮な気持ちで楽しみながらコードを書いていたのですが、ふと手が停まってしまいました。イテレータのテストコードを書こうとして。

はたと気付いたのですが、どうやら私はこれまでの間、自分で書いたイテレータ (ブロック付きメソッドですか) のテストコードを書いたことが無かったのかもしれません。

イテレータは、

obj.get_elem(target) { |elem| some_process (elem); ... };

の様に利用する訳ですから、テストコードも、

obj.get_elem(target) { |elem| assert_equal(EXPECTS_VALUE, elem) };

の様に書ければ、何の問題もないでしょう。
ところが、このイディオムは、テストコードにそのまま適用することはできません。

繰り返し処理で渡される要素の内容は、要素ごとに異なる訳ですから、期待値も同様に複数の値を順次、切り替えてやる必要がありますね。

取り敢えず、単純に実装してみます。

$ cat iter_test.rb
class Iter_Test < Array

  def initialize arg
    @data = arg;
  end

  def get regexp
    @data.select { |line|       # 条件に合致した要素のみ、戻り値に含まれる。
      yield line  if line =~ regexp;
    }
  end
end

# Ends here.

といった感じのクラスを想定してみます。
インスタンス生成時に与えられた配列をコレクションとして保持するクラスで、`get' メソッドは、与えたパターンにマッチする要素だけを返すコレクション操作メソッドです。

一番単純なテストの実装としては、

$ cat test_iter_test.rb
require 'test/unit'
require 'iter_test'

class TC_DefineReader < Test::Unit::TestCase
  def setup
    # テストデータの配列を定義
    @test_data = [
      "* start line.\n",
      "# comment line.\n",
      "{ header start line.\n",
      "} header end line.\n",
      "/ separater line.\n",
      "[ block start line.\n",
      "] block end line.\n",
      "	 body line.\n",
    ];
    # イテレータに渡すパターンを定義
    @valid_regexp = /^[\{\}\[\]]/;     # valid.
    @invalid_regexp = /^[\{\}\[\]\s]/; # number of actuals more than expects.
  end

  def test_by_array
    # 配列を直接参照する方法。

    # テスト対象のオブジェクト生成
    obj = Iter_Test.new(@test_data);

    # 期待値を順次参照してテストを実施。
    idx = 0;
    expects = [
      "{ header start line.\n",
      "} header end line.\n",
      "[ block start line.\n",
      "] block end line.\n",
    ];
    obj.get(@valid_regexp) { |l| # valid.
      assert_equal(expects[idx], l);
      idx += 1;
    };
  end
end

の様なものが考えられます。
イテレータが返す期待値を予め配列に保持しておき、イテレータに渡すブロック内で、順次、配列の内容を参照しながら assert して行きます。

フィクスチャとして、先頭カラムに記号か空白が置かれた行 (改行文字を終端とする文字列) を保持する配列を用意し、先頭カラムの文字によって行を選択する正規表現イテレータに渡しています。ここでは `{', `}', `[', `]' が行頭に置かれた行を要素とするコレクションが返されることになります。

テストコードは、上記に該当する行を期待値として保持する配列を用意し、それらと合致する要素が返されるかを検証するコードになっています。

このコードを実行すると、

$ ./test_iter_test.rb
Loaded suite ./test_iter_test
Started
.
Finished in 0.140324 seconds.

1 tests, 4 assertions, 0 failures, 0 errors

となり、正しい事が確認できました。

異常なケースはどうでしょうか。
テストデータを少し変更して、定義した期待値と異なる要素が返される様にしてみます。

$ head -20 test_iter_test.rb
require 'test/unit'
require 'iter_test'

class TC_DefineReader < Test::Unit::TestCase
  def setup
    # テストデータの配列を定義
    @test_data = [
      "* start line.\n",
      "# comment line.\n",
      "{ header start line.\n",
      # "} header end line.\n",
      "/ separater line.\n",
      "[ block start line.\n",
      "] block end line.\n",
      "	 body line.\n",
    ];
    # イテレータに渡すパターンを定義
    @valid_regexp = /^[\{\}\[\]]/;     # valid.
    @invalid_regexp = /^[\{\}\[\]\s]/; # number of actuals more than expects.
  end

本来であれば二つ目の要素として返されるデータをコメントアウトして実行してみます。

$ ruby test_iter_test.rb
Loaded suite test_iter_test
Started
F
Finished in 0.189173 seconds.

  1) Failure:
test_by_array(TC_DefineReader)
    [test_iter_test.rb:54:in `test_by_array'
     test_iter_test.rb:53:in `get'
     ./iter_test.rb:13:in `select'
     ./iter_test.rb:13:in `get'
     test_iter_test.rb:53:in `test_by_array']:
<"} header end line.\n"> expected but was
<"[ block start line.\n">.

1 tests, 2 assertions, 1 failures, 0 errors

異常が検出されていますね。しかし、この程度のデータ量でのテストなら異常となったデータを簡単に判別できますが、データ量が非常に多かったり、内容が判別し難いデータ (中身がとても似ている様な) だった場合には、余り親切とは言えないかもしれませんね。
そこで、どの要素が failure したのかをもう少し判り易くしてみます。
最初に紹介したテストコード中の、イテレータにブロックを渡して assert している、

    obj.get(@valid_regexp) { |l| # valid.
      assert_equal(expects[idx], l);
      idx += 1;
    };

というブロックを、

    obj.get(@valid_regexp) { |l| # valid.
      assert_equal(expects[idx], l,
                   "*** Error index: #{idx}"); # 失敗時には添字を出力。
      idx += 1;
    };

と書き換えてみると、

$ ruby test_iter_test.rb
Loaded suite test_iter_test
Started
F
Finished in 0.14927 seconds.

  1) Failure:
test_by_array(TC_DefineReader)
    [test_iter_test.rb:54:in `test_by_array'
     test_iter_test.rb:53:in `get'
     ./iter_test.rb:13:in `select'
     ./iter_test.rb:13:in `get'
     test_iter_test.rb:53:in `test_by_array']:
*** Error index: 1.
<"} header end line.\n"> expected but was
<"[ block start line.\n">.

1 tests, 2 assertions, 1 failures, 0 errors

となり、少し判り易くなりました。(いや、単に添字を表示する様にしただけですが、それでも判別し難いデータだった場合には判り易くなったと言えると思います……)

さて、もう一つ異常なパターンを。
今度は、要素数が正しく無いケースです。この様なテストでは、期待値として定義した配列の要素数と、実際にイテレータが返却するコレクションの要素数は等しくなければなりません。
但し、期待値の要素数が少ない場合は、実際に返された要素と nil とを比較することになるので、今のコードで十分判別可能です。しかし、

$ head -20 test_iter_test.rb
require 'test/unit'
require 'iter_test'

class TC_DefineReader < Test::Unit::TestCase
  def setup
    # テストデータの配列を定義
    @test_data = [
      "* start line.\n",
      "# comment line.\n",
      "{ header start line.\n",
      "} header end line.\n",
      "/ separater line.\n",
      "[ block start line.\n",
      # "] block end line.\n",
      "	 body line.\n",
    ];
    # イテレータに渡すパターンを定義
    @valid_regexp = /^[\{\}\[\]]/;     # valid.
    @invalid_regexp = /^[\{\}\[\]\s]/; # number of actuals more than expects.
  end

の様にして (イテレータが最後に返却する筈のデータをコメントアウトした)、イテレータが返却する要素数の方が少なくなる場合には、

$ ruby test_iter_test.rb
Loaded suite test_iter_test
Started
.
Finished in 0.175241 seconds.

1 tests, 3 assertions, 0 failures, 0 errors

と、正常にテストが終了してしまうことになります。
これでは困りますので、

    obj.get(@valid_regexp) { |l| # valid.
      assert_equal(expects[idx], l,
                   "*** Error index: #{idx}"); # 失敗時には添字を出力。
      idx += 1;
    };
    # 用意した全ての期待値について検証が行なわれたか。
    assert_equal(expects.size, idx, "*** number of expects more than actuals.");

と、各要素を順に確認するブロックを終えた後に、期待値として定義したデータを全て検証したかの確認を入れておきます。
すると、

$ ruby test_iter_test.rb
Loaded suite test_iter_test
Started
F
Finished in 0.190942 seconds.

  1) Failure:
test_by_array(TC_DefineReader) [test_iter_test.rb:59]:
*** number of expects more than actuals.
<4> expected but was
<3>.

1 tests, 4 assertions, 1 failures, 0 errors

期待した要素数と異なることがレポートされるので一安心ですね。

と、ここまでは配列を定義して、その要素を順に検証して行く方法を採ってみました。
しかし、配列を裸で使うのも芸が無い気がします。(いや、そもそも私は芸なんざ持ってないですけども)
もう少しスマートな方法は無いでしょうか。