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

久し振りに、仕事絡みで 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

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

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

クロージャにしてみる。

ということで、裸の配列を順に参照している部分を、クロージャにしてみようかと。
そもそも、配列をそのまま参照することが悪い訳では決して無いです。しかし、今回例示している様な簡単なイテレータならばそれでも良いですが、何やら複雑怪奇なメソッドだった場合には、配列をそのまま参照という単純な構造ではなく、手続きを内包するクロージャの様な概念を持ち込むことで解決できることがあるのではないか、という希望的観測に基づく実験的試みです。(遊んでいるだけとも言えるかもしれませんが……)

ただ、本来、私はテストコードはシンプルであるべき、と考えていますので、余り複雑なテストコードにはしたくありません。
それでも書いてみるというのは、やはり単なる遊びなのかもしれません。

さて、上のエントリで書いたテストコードを、そのままクロージャにしてみます。どんどん長くなってしまっているので一気に行きます。

$ 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_closure
    # クロージャを作ってアクセサとする方法。

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

    # 期待値を返すクロージャを生成するメソッドを定義。
    # 併せて「返した期待値のインデックス」を返すアクセサも返す。
    expects = [
      "{ header start line.\n",
      "} header end line.\n",
      "[ block start line.\n",
      "] block end line.\n",
    ];
    def make_expect expt
      i = 0;
      e = nil;
      [                         # 多値で返す。
        lambda {
          e = expt[i];
          i += 1;
          e;
        },
        lambda {
          i - 1;
        }
      ]
    end
    get_expect, get_num = make_expect(expects);

    # 期待値を順次取得してテストを実施。
    obj.get(@valid_regexp) { |l| # valid.
      assert_equal(get_expect.call, l,
                   "*** Error index: #{get_num.call}"); # 失敗時には添字を出力。
    };
    # 用意した全ての期待値について検証が行なわれたか。
    assert_nil(get_expect.call, "*** number of expects more than actuals.");
  end
end
$ ruby test_iter_test.rb
Loaded suite test_iter_test
Started
.
Finished in 0.177584 seconds.

1 tests, 5 assertions, 0 failures, 0 errors

となりました。

ミソは、二つのクロージャを生成しているところでしょうか。
先の配列直接参照のテストコードで、assert 失敗時に配列の添字を表示していることと同じことを行なうために、期待値を返すクロージャが直前に返却した期待値の添字を得るためだけのクロージャも生成しています。

配列を直接参照していたコードと比較して、面倒になってるだけな気がします……
まあ、冒頭に書いた様に、もっと複雑なデータ構造をテストする場合に、ひょっとしたら使えるかも……、くらいのものでしかないですかね。

クロージャとか言って、こんなことする位なら、普通にオブジェクト作れば良いだろ、というのは真っ当な意見だと思います。ということで、オブジェクトを生成するバージョンも書いてはあったんですが、もう長くなったので止めにします。クロージャ作るケースと代わり映えしませんしね。

やっぱりシンプルに。

上のエントリでも書きましたが、私自身は、テストコードはシンプルであるべき、複雑なテストコードは百害あって一利無し、と思っていますので、実は、上の様なテストコードはなるべく書かない方が良い、と考えています。
場合によってはアリ、とは思っていますけれど。

やっぱり、

$ 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_collect
    # 配列で一括確認する方法。

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

    # 期待値を一括照合してテストを実施。
    assert_equal([
      "{ header start line.\n",
      "} header end line.\n",
      "[ block start line.\n",
      "] block end line.\n",
    ],
    obj.get(@valid_regexp) { |l| l});
  end
end
$ ruby test_iter_test.rb
Loaded suite test_iter_test
Started
.
Finished in 0.140404 seconds.

1 tests, 1 assertions, 0 failures, 0 errors

とかで宜しいのではないでしょうか。

但し、イテレータに渡したブロックの中で順次取得できる要素と、イテレータの戻り値となるコレクションとでは内容が異なることがあり得ます。
select, collect, each などが戻り値で返す要素のパターンが異なりますし、そもそもどの様にイテレータを実装するのかに依存しますよね。

それも含めて、より厳密なテストとするならば、ブロック内での要素の参照と、最終的に戻り値として取得されるコレクションの両方を確認すべきなのかもしれません。

Rubyist の皆さんは、どの様にテストされておられるのでしょう。