Gauche:testについて

Gauche:testについて

Shiro(2009/07/07 02:59:40 PDT): Shibuya.lisp TT#3の懇親会にて、gauche.testの機能追加の話がちょっと出たので考えてみた。 使い勝手が良くなるなら機能を足すこと自体は問題ない。ただ、やたらにAPIを増やすのは却ってわかりづらいので、組み合わせで済むものなら(よっぽど便利にならない限りは)足さないんじゃないかな、というのはある。 なので、私があまり積極的でない機能を足してほしいという方は、「こういう理由で便利なんだよ!」と私を説得してくださいな。

入れたいな、という機能

異常系のテストで、想定したconditionが上がっていることを簡単にテストしたい

0.8.14までは<test-error>オブジェクトのひとつのインスタンスが*test-error* に束縛されてて、結果として上がってくるものが<test-error>かどうかを 調べる、という形が推奨だったんですが:

   (test* name *test-error* エラーを上げる式)

これには2つ欠点がありました。

  (test* name <read-error>
         (guard (e (else (class-of e)))
            expr))

これについては既に次のように改善してtrunkにコミットしてあります。

  1. (test-error &optional condition-type) という手続きで、 新しい<test-error>オブジェクトを生成できる。
  2. test関数、test*マクロのoptionalな手続きを「比較」ではなく「チェック手続き」という セマンティクスにした。「チェック手続き」は常にexpectedとテスト式の結果を この順で受け取り、結果がexpectedに「沿って」いれば#tを返す。 <test-error>オブジェクト以外のものに対するデフォルトのチェック手続きはequal? なんだけど、このセマンティクスだとexpectedと結果が(equal?のように)対等ではない、 というのがポイント。
  3. <test-error>オブジェクトのチェック手続きは次のセマンティクスを持つ:
    「expectedと結果の双方が<test-error>オブジェクトで、 expectedの方のcondition-typeが無指定か、結果のcondition-typeの スーパータイプなら#t」

これを使うと、例えばexprが<read-error>を上げるかどうかはこう書ける:

  (test* name (test-error <read-error>) expr)

exprが上げるのは<read-error>のサブクラスであっても構いません。

expected failure

failすることが期待されるテストって矛盾してるじゃん、 そもそも期待されてるなら、その結果をexpectすれば単なるsuccessになるから 意味ないじゃん、と思っていたのですが、 「環境によってはfailする場合がある」というテストを 書いておくのに便利だな、ということに気づきました。

lint的なチェック

これはtest-moduleの発展形なんだけれど、 koguroさんの[glint http://homepage.mac.com/naoki.koguro/prog/codecheck/index-j.html]的 なことをテスト時にやれないか、ということです。

Gaucheでは実行時のon-the-flyのコンパイル時間が短いことも重要なので、 実行時compileであまり重いチェックが出来ません。 でもテスト時なら少々時間を食ってもいいので、 グローバルな依存関係を見れば、手続きの引数の違いや 型の違いなどまである程度追えるんじゃないかと思います。

結果表示時に、長い表示をtruncate

テストが失敗した場合、標準出力に出るサマリに失敗した結果が含まれますが ("discrepancy found" のところ)、実行結果が巨大だとこれがかなり見にくいです。 例えば大きなSXMLを生成するのとか。Common Lispの省略記法みたいな感じで 短縮できるといいかな、と思ってます。 必要があればログを見てもらうと全部出てる、ということで。

入れるかも、という機能

部分的にテストを走らせる

特定のfailureだけ潰している時は、テスト全部を毎回走らせるのは 重いことがあります。

指定した文字列やregexpにマッチする名前を持つ test-sectionだけ走らせる、というのはできるかな、と思ってます。

テストの前処理や後処理に副作用を使ってる場合、 それがトップレベルにべたで書いてあると選択的に実行するのが難しくなりますが、 そのへんも含めて構造化する構文を導入すべきか、 それともトップレベルの実行を何らかの方法でハックするか、というのは 思案中ですが。

嬉しさがよくわからないので入れないだろうな、という機能

結果が#tであること、#fであること、などをテストする手続き

JUnitとかにはassertTrueとかassertFalseとかありますが、Schemeのような 無名関数が簡単にかける言語でこれらを使うメリットがよくわからないんですね。 というのは、

  1. exprが#tになることをテストする(test-true name expr)という 手続きは (test* name #t expr) と書いてもほとんど同じ
  2. test-trueでは、テストが失敗したときに「それが失敗したこと」しかわからない。 exprの中で「あるアクションの結果が期待したナニカに沿っているかどうか」を 判断して#t/#fを返しているなら、それはむしろ次のように と書くべきで、これなら失敗した時に結果を返す式の結果がログに出るので 原因究明がしやすい:
  (test* name ナニカ 結果を返す式 rがナニカに沿っているかどうかテストする手続き)

で、JavaやC++ではこのrがナニカに沿っているかどうかテストする手続きを さらっと書くのが面倒だからわざわざassertTrueが用意してあるんじゃないか、 と私は思っていましたよ。

今のGaucheでも、

  (test* name #t test-expr)

と書くのは本来は述語をテストしたい場合だけに止め、できる限りexpectedの ところには具体的な値を書くべきじゃないか、と思ってます。つい#tにしたくなっちゃう 気持ちはわかるし、私もそういうテストをたくさん書いてるんですが、 例えばmy-funcの戻り値が文字列であることをテストしたいなら、

  (test* name #t (string? (my-func)))

ではなく、

  (test* name <string> (class-of (my-func)))

あるいは

  (test* name <string> (my-func) (lambda (t r) (is-a? r t))

と書くのがいいんじゃないかと。

で、これだと長くなっちゃうんで、最後のやつをマクロにして

  (test-type* name type expr) 

と書けるようにする、というのはありだと思います。

コメント・議論

Past comment(s)

kou (2009/07/07 04:39:20):

『「それが失敗したこと」しかわからない。』のが問題だというのは私もそう思います。

私は「結果を返す式の結果」だけでなく、「結果がどうして期待値と違ったか」もわかると嬉しいと思います。

なので、

  (test* name #t (string? (my-func)))

はこう書けるのが嬉しいと思っています。

  (test-predicate* name string? (my-func))

(string?が前にきているのはstring?が期待値っぽいから)

そして、失敗したときはこんな風に表示してくれる:

  name: <(string? "(my-func)の戻り値")> should be #t

(あ、GaUnitにはこの機能入れていない。。。)

assertTrueは結果を見たときではなくて、テストコードを見たときに「真偽値を返すことをテストしている」ということがわかりやすいからかと思っていました。

私は、

  (test-true* name (string? (my-func)))

の方が

  (test* name <string> (my-func) (lambda (t r) (is-a? r t)))

よりも、「テストコードを見たとき」に何をテストしようとしているのかがわかりやすく感じます。(もちろん、↑がmy-funcのテストではなく、string?のテストである場合。my-funcのテストの場合はtest-type*の方がよいと思う。でも、型をチェックするテストよりも中身をチェックするテストの方が良さそうな気がする。)

kou (2009/07/07 04:42:37):

truncateされて、表示されている部分だけ同じで、ログを見ないといけないと面倒くさくなってテストが嫌になってしまう気がするので、diffを表示するのがよいと思います。(GaUnitでは長くなくても必要そうならdiffを表示している)

shiro (2009/07/07 05:10:03):

diffを…というのはexpectedとresultのdiffってことでしょうか。 equal?テストならいいんだけど、web appのテストみたいにexpectedの方がパターンになってる場合は何を見るべきか難しそうです。むしろfailした場合に何を表示するかってところもカスタマイズできる方がいいかも。

shiro (2009/07/07 05:15:20):

test-predicate*はありですね。test-satisfy*でもいいかな。結局、「値」で語彙を増やすというより「何を見ているか」で分類するのが良さそうな。

koguro (2009/07/07 06:32:59):

こうできたらいいな、というのが2つほどあります。 (1) テスト実行中にエラーが発生したら、スタックトレースも出力する。今は、単にエラーが発生した、ということしか分からないので原因を特定するのが面倒に感じます。 (2) カバレッジ率も測定する。マクロがあるので、ソースコードレベルでのカバレッジの計算は難しいと思うのですが、VMのバイトコード単位でもいいんで、「ここが実行されていない」という情報が得られたらうれしいです。

higepon (2009/07/07 07:44:53):

ああ。カバレッジは自分も欲しいです。

naoya_t (2009/07/07 08:36:48):

カバレッジ欲しい!にもう1票。

shiro (2009/07/07 15:44:27):

@koguro: 環境変数GAUCHE_TEST_REPORT_ERRORをセットして出るやつとは別に、ということですか? それとも常に出すのがいい? カバレッジは確かに欲しいですね。せっかく自前でVM持ってるんだからやるべきかなあ。

koguro (2009/07/08 06:04:29):

すみません、GAUCHE_TEST_REPORT_ERRORの存在を知りませんでした。ちゃんとドキュメント読まねば。

Post a comment

Name:

More ...