アプリケーションサーバーで実行されていない場合、ユニットテストはどのようにデータソースを設定する必要がありますか?
-
06-07-2019 - |
質問
ご協力いただきありがとうございます。私のアプローチ全体が間違っていたこと、または低レベルのコードがコンテナで実行されているかどうかを知る必要がないことを示す回答を投稿しました(予想どおり)。私は同意する傾向があります。ただし、私は複雑なレガシーアプリケーションを扱っており、現在の問題に対して主要なリファクタリングを行うオプションがありません。
一歩下がって、元の質問の動機となる質問をします。
JBossの下で実行されるレガシーアプリケーションがあり、低レベルのコードにいくつかの変更を加えました。変更用の単体テストを作成しました。テストを実行するには、データベースに接続する必要があります。
レガシーコードはこの方法でデータソースを取得します:
(jndiNameは定義済みの文字列です)
Context ctx = new InitialContext();
DataSource dataSource = (DataSource) ctx.lookup(jndiName);
私の問題は、このコードを単体テストで実行すると、コンテキストにデータソースが定義されていないことです。これに対する私の解決策は、アプリケーションサーバーで実行されているかどうかを確認し、そうでない場合は、テストDataSourceを作成して返すことです。アプリサーバーで実行している場合は、上記のコードを使用します。
それで、私の本当のの質問は、これを行う正しい方法は何ですか?ユニットテストが適切なデータソースを返すようにコンテキストを設定して、テスト中のコードがどこで実行されているかを知る必要がないように承認された方法はありますか?
コンテキストの場合:私の元の質問:
JBossで実行されているかどうかを知る必要があるJavaコードがあります。コードがコンテナで実行されているかどうかを判断する標準的な方法はありますか?
最初のアプローチは実験によって開発され、初期コンテキストを取得し、特定の値を検索できることをテストすることで構成されています。
private boolean isRunningUnderJBoss(Context ctx) {
boolean runningUnderJBoss = false;
try {
// The following invokes a naming exception when not running under
// JBoss.
ctx.getNameInNamespace();
// The URL packages must contain the string "jboss".
String urlPackages = (String) ctx.lookup("java.naming.factory.url.pkgs");
if ((urlPackages != null) && (urlPackages.toUpperCase().contains("JBOSS"))) {
runningUnderJBoss = true;
}
} catch (Exception e) {
// If we get there, we are not under JBoss
runningUnderJBoss = false;
}
return runningUnderJBoss;
}
Context ctx = new InitialContext();
if (isRunningUnderJboss(ctx)
{
.........
今、これは機能しているように見えますが、ハックのように感じます。 「正しい」とはこれを行う方法?理想的には、JBossだけでなく、さまざまなアプリケーションサーバーで動作する方法が欲しいです。
解決
アプローチ全体が私に向かって間違っていると感じています。アプリで実行しているコンテナを知る必要がある場合、何か間違ったことをしています。
Springを使用すると、何も変更せずにTomcatからWebLogicに移動できます。適切に構成すれば、JBOSSでも同じトリックができると確信しています。それが私が狙う目標です。
他のヒント
全体の概念は前から後ろにあります。下位レベルのコードは、この種のテストを行うべきではありません。別の実装が必要な場合は、関連するポイントに渡します。
依存性注入のいくつかの組み合わせ(Spring、configファイル、またはプログラム引数を使用するかどうか)とファクトリーパターンは、通常、最適に機能します。
例として、耳または戦争が開発環境、テスト環境、または実稼働環境のどちらに行くかによって、設定ファイルを設定するAntスクリプトに引数を渡します。
おそらくこのようなもの(いですが動作するかもしれません)
private void isRunningOn( String thatServerName ) {
String uniqueClassName = getSpecialClassNameFor( thatServerName );
try {
Class.forName( uniqueClassName );
} catch ( ClassNotFoudException cnfe ) {
return false;
}
return true;
}
getSpecialClassNameFor メソッドは、各Application Serverに固有のクラスを返します(アプリサーバーが追加されると新しいクラス名を返す場合があります)
次に、次のように使用します:
if( isRunningOn("JBoss")) {
createJBossStrategy....etcetc
}
Context ctx = new InitialContext(); DataSource dataSource = (DataSource) ctx.lookup(jndiName);
InitialContextを構築するのは誰ですか?その構造は、テストしようとしているコードの外部にある必要があります。そうしないと、コンテキストをモックできなくなります。
レガシーアプリケーションで作業していると言ったので、最初にコードをリファクタリングして、コンテキストまたはデータソースをクラスに簡単に依存関係注入できるようにします。そうすれば、そのクラスのテストをより簡単に書くことができます。
クラスを構成するコードをリファクタリングするまで、以下のコードのように2つのコンストラクターを使用してレガシーコードを移行できます。これにより、Fooをより簡単にテストでき、Fooを使用するコードを変更せずに維持できます。その後、コードをゆっくりとリファクタリングして、古いコンストラクターを完全に削除し、すべての依存関係を依存関係に注入できます。
public class Foo {
private final DataSource dataSource;
public Foo() { // production code calls this - no changes needed to callers
Context ctx = new InitialContext();
this.dataSource = (DataSource) ctx.lookup(jndiName);
}
public Foo(DataSource dataSource) { // test code calls this
this.dataSource = dataSource;
}
// methods that use dataSource
}
しかし、そのリファクタリングを始める前に、背中をカバーするためにいくつかの統合テストが必要です。そうしないと、DataSourceルックアップをコンストラクターに移動するなどの単純なリファクタリングでさえ、何かが壊れるかどうかを知ることができません。その後、コードが改善され、テスト可能になると、単体テストを作成できます。 (定義により、テストがファイルシステム、ネットワーク、またはデータベースに触れる場合、それは単体テストではなく、統合テストです。)
単体テストの利点は、1秒あたり数百または数千という高速で実行され、一度に1つの動作のみをテストすることに重点が置かれていることです。これにより、頻繁に実行できるようになり(1行を変更した後、すべてのユニットテストの実行をためらうと実行が遅くなりすぎます)、素早いフィードバックが得られます。そして、それらは非常に集中しているため、失敗したテストの名前を見るだけで、実稼働コードのどこにバグがあるかがわかります。
統合テストの利点は、すべての部品が正しく接続されていることを確認することです。それも重要ですが、データベースに触れるなどの処理が非常に遅くなるため、頻繁に実行することはできません。ただし、継続的インテグレーションサーバーで少なくとも1日に1回は実行する必要があります。
この問題に取り組む方法はいくつかあります。 1つは、ユニットテスト中にContextオブジェクトをクラスに渡すことです。メソッドシグネチャを変更できない場合は、初期コンテキストの作成を保護されたメソッドにリファクタリングし、メソッドをオーバーライドして、モックされたコンテキストオブジェクトを返すサブクラスをテストします。これにより、少なくともクラスをテスト対象にできるため、そこからより適切な代替手段にリファクタリングできます。
次のオプションは、データベース接続をファクトリーにして、コンテナー内にあるかどうかを判断できるようにし、それぞれの場合に適切な処理を実行することです。
考慮すべきことの1つは、コンテナからこのデータベース接続を取得したら、それをどうするかということです。簡単ですが、データアクセスレイヤー全体を実行する必要がある場合、単体テストではありません。
単体テストでレガシーコードを移動するこの方向でのさらなる支援については、Michael Featherのレガシーコードの効果的な使用。
これを行うクリーンな方法は、ライフサイクルリスナーを web.xml
で構成することです。これらは必要に応じてグローバルフラグを設定できます。たとえば、 ServletContextListener を定義できます。 web.xml
および contextInitialized
メソッドで、コンテナ内で実行しているグローバルフラグを設定します。グローバルフラグが設定されていない場合、コンテナ内で実行されていません。