単一化でテストすれば良いじゃない

最近、こんなテストをいくつか書いていて、イラっとしました。

post(:create_note, { :note => { ... } })
assert_equal(
  {
    "id" => 13,
    "created_at" => Time.now,
    "updated_at" => Time.now,
    ....
  }, JSON.parse(@response.body))

このテストにはいくつか問題があります。

  1. idは13だと思っていて大丈夫か?
  2. postした時刻とassert_equalする時刻は等しいか?
  3. created_atとupdated_atが等しいことはテストできるか?

どれも保証されません。*1


さて、これは、なんかどこかで見たことがあります。

まず連想するのはMLやHaskellのパターンマッチです。(ここにOCamlの例を書こうと思ったけど、忘れていたので省略。)パターンマッチには制限があって、3番目の制約「created_atとupdated_atが等しい」は記述することができません(一つのパターンの中に同じ変数を複数回書くことはできません)。ちょっと弱い。

で、ツリーオートマトンじゃないかとか、正規表現で行けるんじゃないかとか、いろいろ考えたんですけど、かなりぴったりのものを見つけました。

単一化で良いんじゃないか。


こんな感じで書きましょう。

post(:create_note, { :note => { ... } })
assert_unify(
  {
    "id" => :"'a",
    "created_at" => :"'b",
    "updated_at" => :"'b",
    ....
  }, JSON.parse(@response.body))

ここではメタ変数として、'a、'bを導入します。'aと'bは、それぞれなんでも良いんだけど、'bに対応するものは、それぞれの出現で等しくないといけません。*2

この例だと、idはなんでも良いんだけどなんかidというフィールドが無いことは許されません。created_atとupdated_atもなんでも良いんだけど、この二つは等しくないといけません。


こんな感じ。

今後の課題

Occur checkしていないので、変なことになることがあります。これはKnown issue。

メタ変数が束縛する値について条件を書きたいことがあるかも。

assert_unify(..., ...) do |env|
  assert env[:"'a"].is_a(Integer)
  ...
end

とか書く感じかなー。

メタ変数名を考えるのがめんどくさいので、どうでも良いやつはワイルドカード的なパターンを書けると良いかも。これもOCamlに倣うなら、:"_"かな。

あとメタ変数のシンタクスが書きにくすぎて生きるのが辛い。どうしようか。

*1:2,3番目の問題については、Timeをのっとるソリューションがいくつかあるようです。

*2:OCamlの型変数に倣って、メタ変数はシングルクォートで始まるシンボルとしましたが、これは書きにくい上に読み難いので、なんとかするべきかも……