単体テストでReflectionを使用するのは悪い習慣ですか?
過去数年間、私はJavaでは単体テストでReflectionが広く使用されていると常に考えていました。チェックする必要のある変数/メソッドの一部はプライベートであるため、それらの値を読み取る必要があります。 Reflection APIはこの目的にも使用されるといつも思っていました。
先週、いくつかのパッケージをテストする必要があったため、いくつかのJUnitテストを作成しました。いつものように、私はリフレクションを使用してプライベートフィールドとメソッドにアクセスしました。しかし、コードをチェックした私のスーパーバイザーはそれに満足しておらず、Reflection APIはそのような「ハッキング」に使用するためのものではないと言っていました。代わりに、生産コードの可視性を変更することを提案しました。
Reflectionを使用するのは本当に悪い習慣ですか?本当に信じられない
編集:私はすべてのテストがtestと呼ばれる別のパッケージに含まれている必要があることを言及したはずです(したがって、保護された可視性を使用することも可能な解決策ではありませんでした)
私見リフレクションは、実際には最後の手段であり、レガシーコードまたは変更できないAPIを単体テストする特別なケースのために予約されています。独自のコードをテストする場合、Reflectionを使用する必要があるという事実は、設計がテスト可能でないことを意味するため、Reflectionに頼る代わりに修正する必要があります。
単体テストでプライベートメンバーにアクセスする必要がある場合、通常、問題のクラスに不適切なインターフェイスがあるか、またはやりすぎていることを意味します。そのため、インターフェースを修正するか、いくつかのコードを別のクラスに抽出し、問題のあるメソッド/フィールドアクセサーを公開する必要があります。
一般にReflectionを使用すると、コードの理解と保守が難しくなるだけでなく、より脆弱になることに注意してください。通常の場合はコンパイラによって検出されるエラーのセット全体がありますが、Reflectionでは、ランタイム例外としてのみ発生します。
更新: @tacklineが指摘したように、これは、テストフレームワークの内部ではなく、自分のテストコード内でのみReflectionを使用することに関するものです。 JUnit(およびおそらく他のすべての類似フレームワーク)は、リフレクションを使用してテストメソッドを識別し、呼び出します-これは、リフレクションの正当化およびローカライズされた使用です。 Reflectionを使用せずに同じ機能と利便性を提供することは困難または不可能です。 OTOHはフレームワークの実装内に完全にカプセル化されているため、独自のテストコードを複雑にしたり侵害したりすることはありません。
テストのためだけに本番APIの可視性を変更するのは本当に悪いです。この可視性は、正当な理由により現在の値に設定される可能性が高く、変更されません。
単体テストにリフレクションを使用することはほとんど問題ありません。もちろん、 テストしやすいようにクラスを設計する にする必要があります。これにより、必要な反射が少なくなります。
たとえば、SpringにはReflectionTestUtils
があります。しかし、その目的は依存関係のモックを設定することであり、そこでは春がそれらを注入することになっていた。
トピックは「do&do n't」よりも深く、whatをテストする必要がある-オブジェクトの内部状態をテストする必要があるかどうかしない;テスト対象のクラスの設計に質問する余裕があるかどうか。等.
TDDの観点から-テスト駆動設計-これは悪い習慣です。このTDDにタグを付けておらず、具体的に質問もしていないことは知っていますが、TDDは良い習慣であり、これはその穀物に反します。
TDDでは、テストを使用してクラスのインターフェイス(publicインターフェイス)を定義します。したがって、パブリックインターフェイスとのみ直接対話するテストを記述しています。そのインターフェイスに関心があります。適切なアクセスレベルは、設計の重要な部分であり、優れたコードの重要な部分です。プライベートなものをテストする必要がある場合、通常、私の経験では、デザインの匂いです。
私はそれを悪い習慣と見なしますが、実動コードの可視性を変更するだけでは良い解決策ではありません。原因を調べる必要があります。テストできないAPIがある(つまり、テストでハンドルを取得するのに十分なAPIが公開されていない)ため、代わりにプライベート状態をテストしようとしているか、テストが実装と結合しすぎているため、リファクタリングするときは、わずかな使用のみ。
あなたのケースについてもっと知ることなく、私は実際にそれ以上言うことはできませんが、実際には reflection を使用するのは貧しい習慣と見なされています。個人的には、テストをリフレクションに頼るよりもテスト中のクラスの静的な内部クラスにしたい(APIのテスト不能な部分が私の制御下になかったと言う場合)が、一部の場所ではテストコードに大きな問題がありますリフレクションを使用するよりも、本番コードと同じパッケージ。
編集:編集に応じて、thatは少なくともReflectionを使用するのと同じくらい悪い習慣で、おそらくもっと悪いでしょう。通常の処理方法は同じパッケージを使用することですが、テストは別のディレクトリ構造に保持します。単体テストがテスト対象のクラスと同じパッケージに属していない場合、どうすればいいのかわかりません。
とにかく、次のようなサブクラスをテストすることで、protected(残念ながら、これには本当に理想的なpackage-privateではありません)を使用することで、この問題を回避できます。
public class ClassUnderTest {
protect void methodExposedForTesting() {}
}
そしてユニットテストの内部
class ClassUnderTestTestable extends ClassUnderTest {
@Override public void methodExposedForTesting() { super.methodExposedForTesting() }
}
そして、保護されたコンストラクタがある場合:
ClassUnderTest test = new ClassUnderTest(){};
通常の状況では必ずしも上記を推奨するわけではありませんが、あなたは「ベストプラクティス」ではなく、下で働くように求められている制限事項です。
私はBozhoの考えを再確認します:
テストのためだけに実稼働APIの可視性を変更するのは本当に悪いことです。
ただし、 おそらく動作する可能性のある最も単純なこと をしようとすると、少なくともコードがまだ新しい/変更されている間は、Reflection APIの定型文を書くことをお勧めします。つまり、メソッド名またはパラメーターを変更するたびに、テストからの反映された呼び出しを手動で変更する負担は、非常に大きな負担であり、フォーカスが間違っています。
テストのためだけにアクセシビリティを緩和し、他の製品コードからwas-privateメソッドに誤ってアクセスしたという落とし穴に陥った dp4j.jar :Reflection APIコードを挿入します(- Lombok -style )これにより、プロダクションコードを変更したり、Reflection APIを自分で記述したりしないでください。 dp4jは、ユニットテストでの直接アクセスを、コンパイル時に同等のリフレクションAPIに置き換えます。 JUnitを使用したdp4jの例 です。
コードは2つの方法でテストする必要があると思います。ユニットテストでパブリックメソッドをテストする必要があります。これは、ブラックボックステストとして機能します。コードは管理可能な機能(優れたデザイン)に分割されているため、個々のピースをリフレクションでユニットテストして、プロセスとは無関係に動作することを確認する必要があります、これを考えることができる唯一の方法は彼らはプライベートです。
少なくとも、これは単体テストプロセスでの私の考えです。
他の人が言ったことに追加するには、次のことを考慮してください。
//in your JUnit code...
public void testSquare()
{
Class classToTest = SomeApplicationClass.class;
Method privateMethod = classToTest.getMethod("computeSquare", new Class[]{Integer.class});
String result = (String)privateMethod.invoke(new Integer(3));
assertEquals("9", result);
}
ここでは、リフレクションを使用してプライベートメソッドSomeApplicationClass.computeSquare()を実行し、Integerを渡してStringの結果を返します。これにより、JUnitテストが正常にコンパイルされますが、次のいずれかが発生すると実行中に失敗します。
- メソッド名「computeSquare」の名前が変更されました
- メソッドは別のパラメータータイプを受け取ります(たとえば、IntegerをLongに変更する)
- パラメータの数が変化する(例えば、別のパラメータを渡す)
- メソッドの戻り値の型が変更されます(おそらく文字列から整数に)
代わりに、computeSquareがパブリックAPIを介して行うべきことを実行したことを証明する簡単な方法がない場合、クラスは多くのことをしようとしている可能性があります。次のテストを提供する新しいクラスにこのメソッドをプルします。
public void testSquare()
{
String result = new NumberUtils().computeSquare(new Integer(3));
assertEquals("9", result);
}
現在(特に、最新のIDEで利用可能なリファクタリングツールを使用する場合)、メソッドの名前を変更してもテストに影響はありません(IDEはJUnitテストもリファクタリングするため) 、パラメーターの種類、パラメーターの数、またはメソッドの戻り値の型を変更すると、Junitテストでコンパイルエラーのフラグが立てられます。つまり、コンパイルはされますが、実行時に失敗するJUnitテストはチェックインされません。
私の最後のポイントは、特にレガシーコードで作業しているときに、新しい機能を追加する必要がある場合、別のテスト可能な適切に記述されたクラスでそれを行うのは簡単ではない場合があることです。この場合、JUnitテストコードで直接実行できる保護された可視性メソッドへの新しいコード変更を分離することをお勧めします。これにより、テストコードのベースの構築を開始できます。最終的には、クラスをリファクタリングして追加機能を抽出する必要がありますが、その間に、新しいコードの保護された可視性が、主要なリファクタリングなしでテスト容易性の観点から最善の方法になる場合があります。