シリーズの3番目の記事とメインシリーズの小さなブランチ-今回は、Spring統合テストライブラリの構築方法と動作、テストの開始時の動作、テスト用にアプリケーションとその環境を微調整する方法を示します。
統合テストでPostgresなどの実際のベースを使用する方法についてのHixon10 コメントによって、この記事を書くように促されました。 コメントの著者は、便利な全包含ライブラリーembedded-database-spring-testの使用を提案しました。 そして、私はすでにコードの段落と使用例を追加しましたが、それについて考えました。 もちろん、既製のライブラリを取得することは正しくて良いことですが、もし目標がSpringアプリケーションのテストの書き方を理解することであるならば、同じ機能を自分で実装する方法を示すことはより便利です。 まず、これはSpring Testの内部にあるものについて話す絶好の機会です。 第二に、サードパーティのライブラリに依存することはできないと考えています。サードパーティのライブラリがどのように配置されているのか理解していない場合、これは技術の「魔法」の神話の強化につながるだけです。
今回はユーザー機能はありませんが、解決する必要がある問題があります- ランダムなポートで実際のデータベースを起動し、アプリケーションをこの一時データベースに自動的に接続し、テスト後にデータベースを停止して削除します。
最初は、すでに慣習として、少し理論。 bin、context、configurationの概念にあまり精通していない人には、例えば、私の記事「 Spring / Habrの裏側」で知識を更新することをお勧めします。
ばね試験
Spring TestはSpring Frameworkに含まれるライブラリの1つです。実際、統合テストに関するドキュメントのセクションで説明されているものはすべてそれだけです。 ライブラリが解決する4つの主なタスクは次のとおりです。
- テスト間のSpring IoCコンテナーとそのキャッシュを管理する
- テストクラスに依存性注入を提供する
- 統合テストに適したトランザクション管理を提供する
- 開発者が統合テストを作成するのに役立つ基本クラスのセットを提供する
公式のドキュメントを読むことを強くお勧めします。多くの便利で興味深いことが書かれています。 ここでは、簡単に絞って、覚えておくと役立つ実用的なヒントをいくつか紹介します。
テストライフサイクル
テストのライフサイクルは次のようになります。
- テストフレームワークの拡張機能(JUnit 4の
SpringExtension
およびJUnit 5のSpringExtension
)は、テストコンテキストブートストラップを呼び出します。 - Boostrapperは、テストとアプリケーションの現在の状態を保存するメインクラスである
TestContext
作成します -
TestContext
はさまざまなフックを設定し(テストの前にトランザクションを開始し、その後にロールバックする)、テストクラス(テストクラスのすべての@Autowired
フィールド)に依存関係を注入し、コンテキストを作成します - コンテキストはコンテキストローダーを使用して作成されます-アプリケーションの基本設定を取得し、テスト設定(重複するプロパティ、プロファイル、ビン、イニシャライザなど)とマージします
- コンテキストは、アプリケーションを完全に記述する複合キー(一連のビン、プロパティなど)を使用してキャッシュされます。
- テスト実行
テストを管理するすべての汚い作業は、実際にはspring-test
によって行われ、Spring Boot Test
、おなじみの@DataJpaTest
や@SpringBootTest
ようないくつかのヘルパークラスを追加しますTestPropertyValues
ような便利なユーティリティは、コンテキストプロパティを動的に変更します。 また、アプリケーションを実際のWebサーバーとして、または(HTTP経由でアクセスすることなく)モック環境として実行できるため、@MockBean
などを使用してシステムコンポーネントを@MockBean
です。
コンテキストキャッシング
おそらく、多くの質問や誤解を招く統合テストの非常に曖昧なトピックの1つは、テスト間のコンテキストキャッシング (上記の段落5を参照)とテストの速度への影響です。 私がよく耳にするコメントは、統合テストは「遅く」、「各テストでアプリケーションを実行する」というものです。 だから、彼らは本当に実行されます-しかし、すべてのテストではありません。 各コンテキスト(つまり、アプリケーションインスタンス)は最大限に再利用されます。 10個のテストが同じアプリケーション構成を使用する場合、アプリケーションは10個のテストすべてに対して1回起動します。 アプリケーションの「同じ構成」とはどういう意味ですか? Spring Testの場合、これはBean、構成クラス、プロファイル、プロパティなどのセットが変更されていないことを意味します。 実際には、これは、たとえば、これら2つのテストが同じコンテキストを使用することを意味します。
@SpringBootTest @ActiveProfiles("test") @TestPropertySource("foo=bar") class FirstTest { } @SpringBootTest @ActiveProfiles("test") @TestPropertySource("foo=bar") class SecondTest { }
キャッシュ内のコンテキストの数は32に制限されています-さらに、LRSUの原則に従って、そのうちの1つがキャッシュから削除されます。
Spring Testがキャッシュのコンテキストを再利用して新しいコンテキストを作成するのを防ぐことができるものは何ですか?
@DirtiesContext
最も簡単なオプションは、テストに注釈が付けられている場合、コンテキストはキャッシュされません。 これは、テストがアプリケーションの状態を変更し、それを「リセット」したい場合に役立ちます。
@MockBean
非常に明白なオプションです。個別にレンダリングしました。@ MockBeanは、コンテキスト内の実際のBeanをMockitoを介してテストできるモックに置き換えます(以下の記事で使用方法を示します)。 重要な点は、この注釈がアプリケーション内のBeanのセットを変更し、 Spring Testに新しいコンテキストを作成させることです。 たとえば、前の例を使用すると、2つのコンテキストが既にここに作成されます。
@SpringBootTest @ActiveProfiles("test") @TestPropertySource("foo=bar") class FirstTest { } @SpringBootTest @ActiveProfiles("test") @TestPropertySource("foo=bar") class SecondTest { @MockBean CakeFinder cakeFinderMock; }
@TestPropertySource
プロパティを変更すると、キャッシュキーが自動的に変更され、新しいコンテキストが作成されます。
@ActiveProfiles
アクティブなプロファイルを変更すると、キャッシュにも影響します。
@ContextConfiguration
そしてもちろん、構成を変更すると、新しいコンテキストも作成されます。
ベースを開始します
今、このすべての知識を持って、私たちはしようとします 離陸する データベースを実行できる方法と場所を理解します。 ここには単一の正しい答えはありません。要件に依存しますが、次の2つのオプションを考えることができます。
- クラス内のすべてのテストの前に1回実行します。
- キャッシュされたコンテキストごとにランダムインスタンスと個別のデータベースを実行します(場合によっては複数のクラス)。
要件に応じて、任意のオプションを選択できます。 私の場合、Postgresが比較的早く起動し、2番目のオプションが適切に見える場合は、1番目のオプションがより難しいものに適している可能性があります。
最初のオプションはSpringではなく、テストフレームワークに関連付けられています。 たとえば、JUnit 5の拡張機能を作成できます。
テストライブラリ、コンテキスト、およびキャッシュに関するすべての知識をまとめると、タスクは次のように要約されます。 新しいアプリケーションコンテキストを作成するときは、ランダムポートでデータベースを実行し、コンテキストに接続データを転送する必要があります 。
ApplicationContextInitializer
インターフェースは、Springで起動する前にコンテキストでアクションを実行します。
ApplicationContextInitializer
インターフェイスには、コンテキストが「開始」される前(つまり、 refresh
メソッドが呼び出される前)に実行されるinitialize
メソッドが1つしかなく、コンテキストの変更(ビン、プロパティの追加)を行うことができます。
私の場合、クラスは次のようになります。
public class EmbeddedPostgresInitializer implements ApplicationContextInitializer<GenericApplicationContext> { @Override public void initialize(GenericApplicationContext applicationContext) { EmbeddedPostgres postgres = new EmbeddedPostgres(); try { String url = postgres.start(); TestPropertyValues values = TestPropertyValues.of( "spring.test.database.replace=none", "spring.datasource.url=" + url, "spring.datasource.driver-class-name=org.postgresql.Driver", "spring.jpa.hibernate.ddl-auto=create"); values.applyTo(applicationContext); applicationContext.registerBean(EmbeddedPostgres.class, () -> postgres, beanDefinition -> beanDefinition.setDestroyMethodName("stop")); } catch (IOException e) { throw new RuntimeException(e); } } }
ここで最初に起こることは、組み込みPostgresがyandex-qatools / postgresql-embeddedライブラリから起動されることです 。 次に、一連のプロパティが作成されます-新しく起動されたベースのJDBC URL、ドライバーのタイプ、スキームのHibernateの動作(自動作成)。 非自明なことの1つはspring.test.database.replace=none
のみspring.test.database.replace=none
これは、H2などの埋め込みデータベースに接続する必要がなく、DataSourceビンを置き換える必要がないことをDataJpaTestに伝えるものです(これは機能します)。
もう1つの重要なポイントはapplication.registerBean(…)
です。 一般に、このBeanはもちろん登録できません。アプリケーションで誰も使用しない場合、特に必要ありません。 登録は、コンテキストが破棄されたときにSpringが呼び出すdestroyメソッドを指定するためにのみ必要です。私の場合、このメソッドはpostgres.stop()
を呼び出してデータベースを停止します。
一般に、それですべてです、もしあれば魔法は終わりました。 次に、この初期化子をテストコンテキストに登録します。
@DataJpaTest @ContextConfiguration(initializers = EmbeddedPostgresInitializer.class) ...
または、便宜上でも、独自の注釈を作成できます。私たちはすべて注釈が大好きだからです!
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @DataJpaTest @ContextConfiguration(initializers = EmbeddedPostgresInitializer.class) public @interface EmbeddedPostgresTest { }
@EmbeddedPostgrestTest
アノテーションが付けられたテストは、ランダムなポートでランダムな名前でデータベースを開始し、このデータベースに接続するようにSpringを構成し、テストの終了時に停止します。
@EmbeddedPostgresTest class JpaCakeFinderTestWithEmbeddedPostgres { ... }
おわりに
Springには神秘的な魔法はなく、「スマート」で柔軟な内部メカニズムがたくさんあることを示したかったのですが、テストとアプリケーション自体を完全に制御できることがわかっています。 一般的に、戦闘プロジェクトでは、テスト用の統合環境をセットアップするためのメソッドとクラスを作成するように全員を動機付けません。既製のソリューションがあれば、それを利用できます。 メソッド全体が5行のコードである場合、おそらく実装を理解していない場合、おそらく依存関係をプロジェクトにドラッグすることは不要です。