真 もわ爛漫

しゃーら、しゃーらしゃーら

Java のテスト、再び!

コメント (テスト用にだけpublicにしたいコンストラクタ、メソッド - 真 もわ爛漫)やトラックバック (Javaのtest - 雑記)をいただいた。結論を言うと「銀の弾丸はない」ということになる。あたりまえか。

一応、私の見解を元にまとめとく。

Reflection を使う

privateだろうとなんだろうとReflectionはそれを破壊出来る。ある種の黒魔術がテストをサポートするというのは感慨深い。
Javaのtest - 雑記 を参考のこと。

この方法は個人的にはあまり好きではない。

  • コンパイル時の各種静的チェックの恩恵から漏れる
  • 統合開発環境リファクタリング機能の恩恵から漏れる
  • そもそもなんで private なのかを考えるのを放棄している
    • コードを理解しようとせずに安易に使ってしまう恐れがある

といった色々なデメリットがあるため。

この方法の優位性は、自分で積極的にインターフェース変更できないライブラリ (ownership が他人にある、有名なAPIの一部であり公開APIについて慎重を期さねばならない等) に対して発揮されるような気がする。バグ報告する際に「コード見てよく分からんかったのだけど、とりあえずこのprivateメソッド、言ってることとやってることが狂ってますぜ」と教えるために使ったりも出来る。

黒魔術ゆえに強力であり、しかしあぶなっかしい。

package private にしてテストも同じパッケージにする。もちろんディレクトリは分ける

Reflectionより素敵で安全。問題はパッケージ名にtestやunit_testと付けられなくなる点だが、Reflectionと比べると諦めるものがより安全より。

「package階層きれいきれい主義者」には合わない。

また、テストケース内での重複をためにテストフレームワークを作って個別のテストケースをシンプルにしたとき、そのフレームワーク統合開発環境上でライブラリの一部に見えてしまうという小さなデメリットもある。ちょっと例を挙げよう

    // Complicated っつーか vCard だ。
    public void testParseComplicatedDataFormatX() {
        ComplicatedDataBuilder builder = new CompricatedDataBuilder();
        builder.addInput("Key1:Value1")
            .addInput("Key2;TYPE=HOME:Value2-1;Value2-2");
        ComplicatedDataParserVerifier verifier =
            new CompricatedDataParserVerifier(
                TestCase.this, builder.build(), new ComplicatedDataParser());
        verifier.addExpected("Key1", "Value1")
            .addExpected("Key2", Arrays.asList("Value2-1", "Value2-2"), new TypeSet("HOME"));
        verifier.verify();
    }

ComplicatedDataParserVerifierなしだと、このテストコードは3倍長くなり、テストケースを一つ追加する度に3倍長いコードをコピペするはめになる (と、しよう =)

ここでComplicatedDataBuilderは本家のライブラリに入れても良いとして、ComplicatedDataParserVerifierはあんまり入れたくない。だが、開発中は両方同じパッケージ上にあるように見えるので「おろ?」と思ってしまうかもしれない。

Eclipseの弱点、という言い方もあるかもしれない

(おまけ)

もっとシンプルに!

    // mVerifier は setUp() で作られて tearDown() で勝手に mVerifier.verify() が呼ばれる。
    public void testParseComplicatedDataFormatX() {
        mVerifier.addInput("Key1:Value1")
            .addInput("Key2;TYPE=HOME:Value2-1;Value2-2");
        mVerifier.addExpected("Key1", "Value1")
            .addExpected("Key2", Arrays.asList("Value2-1", "Value2-2"), new TypeSet("HOME"));
    }

protected にする

もともとC++のprotectedについての解説から拝借したもの。原典は『レガシーコード改善ガイド』。

問題点としては、protectedはtestを意図して使われるものでは本来ないこと。継承したクラスが悪意のあるものであった場合の対策が必須になりprivateのうまみがかなり減じる点

C++ なら friend 使えるんだからこんな例紹介するなや、と思うが、一応一つの選択肢ではある。

public にしてしまう

一見「もしかして、馬鹿なのですか?」と思われるかもしれないが、名著『レガシーコード改善ガイド』も実はこれを推奨している。

private メソッドのテストをどうやって書けばよいでしょうか。これはテストに関連して、しばしば質問される問いの1つです。幸いにも、非常に直接的な回答があります。それは、private メソッドをテストしなければならない場合、そのメソッドは public にすべきだということです。(p152)

なぜこうも「直接的な回答」を推奨しているのかについては本書を読んでいただくとして、「テストしなきゃいけない private って?」と再考するのは意義深い。private メソッドというのは public メソッドに付随するべきもので、public メソッド経由でテストして間に合うはずのコードの中で private メソッドをテストしなくてはならないというのは「その private メソッドはサプリメンタル以上の何か重要な仕事をしている」という意味であり、だったら public にするのが筋が通ってんじゃね、という主張は、ある意味あり。

ただ、公開APIにすると「これを使え」シグナルをよわよわエンジニアにまで発することになるので、有名なライブラリなどを開発/メンテナンスしているときには少し悩むところかもしれない。コメントに「〜〜メソッドからこのメソッドが呼ばれます。このメソッドを通常のユーザが直接使うべきではありません」と書いてあっても使いそうだし。Thread#run() を直接呼んで「(・3・)あるぇー?」と思ったことがある人はなんとなく感じを掴めるかと思うが。

個人的には、リファクタリングしてそのメソッドを static public にして関数型の文字通り「関数」的性質 (オブジェクト内部の状態遷移を持たない性質) を持たせて何かのUtility libraryにそのメソッドを移動させてMock込みのテストにする、まで行くとtestabilityがぐーん、と上がる気がするのだが「お、オブジェクト指向の思想を否定する手法なのです!」とかケチつけられると「もっともで」と言わざるを得ない。

private someMethod() に対して それを呼ぶだけの public someMethodForTest() を書く

すんげー中途半端な解なので非推奨。"ForTest"とつけて紳士協定を意識させる、という意図は理解するが、元のメソッドを public にするのと比べると悲しいほどうまみが少なく、汚い。

追加考察

test用のアノテーションとかがあると良いかもしれないが、現状そういう機能が追加される見込みはなさそうだし、悪意のあるコードがProductionコードでtest用のメソッドを呼び出さないために型理論方面でのアシストが欲しくなる (Production コードから test用メソッドを呼ぶことを禁止するためにはある種の静的型チェックが欲しい)。

まとめ?

銀の弾丸はなかった。