Spring統合テストの最適化

1.はじめに

この記事では、Springを使用した統合テストとそれらを最適化する方法について総合的に説明します。

最初に、Springエコシステムに焦点を当てて、統合テストの重要性と現代のソフトウェアにおけるそれらの位置付けについて簡単に説明します。

後で、Webアプリケーションに焦点を当てて、複数のシナリオを取り上げます。

  • 次に、テストの形成方法とアプリ自体の形成方法の両方に影響を与える可能性があるさまざまなアプローチについて学習することにより、テスト速度を向上させるための戦略について説明します。

始める前に、これは経験に基づく意見記事であることを覚えておくことが重要です。このことのいくつかはあなたに合っているかもしれませんし、そうでないかもしれません。

最後に、この記事ではコードサンプルにKotlinを使用してできるだけ簡潔にしていますが、概念はこの言語に固有のものではなく、コードスニペットはJava開発者とKotlin開発者にとっても意味があります。

2.統合テスト

  • 統合テストは自動テストスイートの基本的な部分です** healthy test pyramid に従うならば、それらは単体テストほど多くあるべきではありません。 。 Springなどのフレームワークに頼ると、システムの特定の動作を危険から解放するために、かなりの量の統合テストが必要になります。

  • Springモジュール(データ、セキュリティ、ソーシャルなど)を使用してコードを単純化するほど、統合テストの必要性が高まります。 ** これは、インフラストラクチャのちょっとしたボブを @ Configuration クラスに移動するときに特に当てはまります。

「フレームワークをテストする」べきではありませんが、フレームワークがニーズを満たすように構成されていることを確認する必要があります。

統合テストは私たちが自信をつけるのに役立ちますが、それらは代償を払います:

  • これは実行速度が遅いため、ビルドが遅くなります

  • また、統合テストはより広いテスト範囲を意味します

ほとんどの場合理想

これを念頭に置いて、上記の問題を軽減するための解決策を見つけようとします。

3. Webアプリをテストする

SpringはWebアプリケーションをテストするためにいくつかの選択肢を持っています、そしてほとんどのSpring開発者はそれらに精通しています。

反応のないWebアプリケーションに便利なサーブレットAPIをモックします。 ** TestRestTemplate :

私たちのアプリを指して使用することができます。 モックサーブレットは望ましくない ** WebTestClient :は

反応したWebアプリケーションのためのテストツール、両方ともモックされた要求/応答または実サーバーへの攻撃

これらのトピックをカバーする記事がすでにあるので、それらについて話すのに時間を費やすことはありません。

もっと深く掘り下げたい場合は、ぜひご覧ください。

4.実行時間の最適化

統合テストは素晴らしいです。彼らは私たちにかなりの自信を与えてくれます。

また、適切に実装されていれば、彼らは我々のアプリの意図を非常に明確な方法で、より少ないモックとセットアップノイズで記述することができます。

しかし、私たちのアプリが成熟し開発が進むにつれて、ビルド時間は必然的に上がります。ビルド時間が長くなるにつれて、毎回すべてのテストを実行し続けることは実用的でなくなる可能性があります。

その後、私たちのフィードバックループに影響を及ぼし、ベストな開発プラクティスへとたどり着きます。

さらに、統合テストは本質的に高価です。ある種の永続化を開始したり、( localhost を離れない場合でも)要求を送信したり、入出力を行うのに時間がかかるだけです。

  • テストの実行を含め、ビルド時間を監視することが最も重要です。それを低く抑えるためにSpringで適用できるいくつかのトリックがあります。

次のセクションでは、ビルド時間の最適化と、その速度に影響を与える可能性がある落とし穴について説明します。

  • プロファイルを賢く使う - プロファイルがパフォーマンスに与える影響

  • __ @ MockBeanの再検討 - __howモッキングがパフォーマンスを向上させる方法

  • リファクタリング @ MockBean - パフォーマンスを向上させるための代替手段

  • @____DirtiesContextについて慎重に考える - 便利だが危険

アノテーションとそれを使わない方法 ** テストスライスを使う - 手助けをしたり私たちの道に着くことができるクールなツール

  • クラス継承を使う - 安全な方法でテストを体系化する方法

  • 状態管理 - フレークテストを回避するためのグッドプラクティス

  • 単体テストにリファクタリングする - しっかりとしなやかにするための最良の方法

造る

始めましょう!

4.1. プロファイルを賢く使う

Profiles はかなりきれいなツールです。つまり、我々のアプリの特定の領域を有効または無効にできる単純なタグです。 implement feature flags とさえ一緒にすることができます!

私たちのプロファイルがより豊かになるにつれて、私たちの統合テストでは時々交換することが魅力的です。 @ ActiveProfiles など、便利なツールがあります。ただし、** 新しいプロファイルでテストを実行するたびに、新しい ApplicationContext が作成されます。

アプリケーションコンテキストを作成することは、何も含まれていないバニラスプリングブートアプリには賢いかもしれません。 ORMといくつかのモジュールを追加すると、すぐに7秒以上に急増します。

たくさんのプロファイルを追加し、それらをいくつかのテストに分散させると、すぐに60秒のビルドが行われます(ビルドの一部としてテストを実行すると仮定します。

複雑なアプリケーションに遭遇すると、これを修正するのは困難です。

ただし、事前に慎重に計画している場合は、適切なビルド時間を維持するのは簡単なことではありません。

統合テストのプロファイルに関しては、いくつかのトリックがあります。

  • 集約プロファイル、つまり test を作成し、必要なすべてのプロファイルを含めます

内 - どこでも私たちのテストプロファイルに固執する ** テスタビリティを念頭に置いてプロファイルを設計します。やらなければならなくなったら

プロファイルを切り替えるには、おそらくもっと良い方法があります。 ** テストプロファイルを一箇所にまとめてください

後 ** すべてのプロファイルの組み合わせをテストしないでください。あるいは、

その特定のプロファイルセットでアプリをテストする環境ごとのe2eテストスイート

4.2. @ MockBean に関する問題

@ MockBean は非常に強力なツールです。

Springの魔法が必要だが特定のコンポーネントをモックしたいときは、 @ MockBean がとても便利です。しかし、それは代償を払って行います。

  • クラスに @ MockBean が出現するたびに、 ApplicationContext キャッシュがダーティとマークされるため、テストクラスの終了後にランナーはキャッシュを消去します。

これは物議を醸すものですが、この特定のシナリオを模倣する代わりに実際のアプリを実行しようとすると解決する可能性があります。もちろん、ここに銀の弾丸はありません。私たちが自分自身に依存関係を偽装させないようにすると、境界はあいまいになります。

私たちは考えるかもしれません:テストしたいのが私たちのREST層だけであるのになぜ私たちは固執するのでしょうか?これは公平な点であり、妥協が常にあります。

ただし、いくつかの原則を念頭に置いて、実際にこれをテストとアプリケーションの両方の設計を改善し、テスト時間を短縮する利点に変えることができます。

4.3. リファクタリング @ MockBean

  • このセクションでは、キャッシュされた ApplicationContext を再利用するために @ MockBean を使用した「遅い」テストをリファクタリングします。**

ユーザーを作成するPOSTをテストしたいとしましょう。 @ MockBean を使用してモックアップしている場合は、サービスが適切にシリアル化されたユーザーで呼び出されたことを確認できます。

私達が私達のサービスを正しくテストすればこのアプローチは十分であるはずです:

class UsersControllerIntegrationTest : AbstractSpringIntegrationTest() {

    @Autowired
    lateinit var mvc: MockMvc

    @MockBean
    lateinit var userService: UserService

    @Test
    fun links() {
        mvc.perform(post("/users")
          .contentType(MediaType.APPLICATION__JSON)
          .content("""{ "name":"jose" }"""))
          .andExpect(status().isCreated)

        verify(userService).save("jose")
    }
}

interface UserService {
    fun save(name: String)
}

私たちは @ MockBean を避けたいのです。それで私たちはエンティティを永続させることになるでしょう(それがサービスがすることであると仮定して)。

ここで最も単純なアプローチは、副作用をテストすることです。POST後、私のユーザーは私のDBに入ります。この例では、JDBCを使用します。

ただし、これはテストの境界に違反します。

@Test
fun links() {
    mvc.perform(post("/users")
      .contentType(MediaType.APPLICATION__JSON)
      .content("""{ "name":"jose" }"""))
      .andExpect(status().isCreated)

    assertThat(
      JdbcTestUtils.countRowsInTable(jdbcTemplate, "users"))
      .isOne()
}

この特定の例では、ユーザーを送信するためにアプリをHTTPブラックボックスとして扱うのでテスト境界に違反しますが、後で実装の詳細を使用してアサートします。

HTTP経由でアプリを実行した場合、HTTP経由でも結果をアサートできますか?

@Test
fun links() {
    mvc.perform(post("/users")
      .contentType(MediaType.APPLICATION__JSON)
      .content("""{ "name":"jose" }"""))
      .andExpect(status().isCreated)

    mvc.perform(get("/users/jose"))
      .andExpect(status().isOk)
}

最後のアプローチに従うと、いくつかの利点があります。

  • 私たちのテストはより早く始まります(間違いなく、もう少し時間がかかるかもしれません)

実行するが、それは返済する必要があります) ** また、私たちのテストでは、HTTPとは関係のない副作用を認識していません

境界、すなわちDB ** 最後に、我々のテストはシステムの意図を明確に表現しています。

POSTすると、ユーザーをGETできるようになります。

もちろん、これはさまざまな理由で常に可能とは限りません。

  • 「副作用」のエンドポイントがない可能性があります。

「テスト用エンドポイント」の作成を検討してください ** 複雑さがアプリ全体を打つには高すぎる:ここでのオプションは

スライスを検討します(後で説明します)。

4.4. @ DirtiesContext について慎重に考える

ときには、テストで ApplicationContext を変更する必要があるかもしれません。このシナリオでは、 @ DirtiesContext がまさにその機能を提供します。

上記と同じ理由で、 __ @ DirtiesContext __は実行時間に関しては非常に高価なリソースなので、注意が必要です。

  • __ @ DirtiesContext __の誤用には、アプリケーションキャッシュのリセットやメモリ内のDBのリセットが含まれることがあります。

4.5. テストスライスを使う

  • テストスライスは1.4で導入されたSpring Bootの機能です。考えはかなり単純です、Springはあなたのアプリケーションの特定のスライスのために減らされたアプリケーションコンテキストを作成します。

また、フレームワークは最小限の設定を行います。

Spring Bootには箱から出してすぐに使えるスライスが沢山あり、私たちも独自のものを作ることができます。

  • _ @ JsonTest: _ JSON関連コンポーネントを登録します

  • @ DataJpaTest :使用可能なORMを含むJPA Beanを登録します

  • @ JdbcTest :生のJDBCテストに役立ち、データソースの面倒を見る

ORMフリルなしのメモリ内DB ** @ DataMongoTest :インメモリMongoテストセットアップを提供しようとします

  • @ WebMvcTest :アプリの他の部分を除いたモックMVCテストスライス

  • …​(確認できます

この特定の機能を賢く使用すれば、特に中小規模のアプリケーションのパフォーマンスに関して大きなペナルティを課すことなく、狭いテストを作成するのに役立ちます。

ただし、アプリケーションが成長し続けると、スライスごとに1つの(小さな)アプリケーションコンテキストが作成されるため、アプリケーションも重なります。

4.6. クラス継承を使う

すべての統合テストの親として単一の AbstractSpringIntegrationTest クラスを使用するのは、ビルドを高速に保つためのシンプルで強力かつ実用的な方法です。

  • しっかりとした設定を提供するなら、私たちのチームは、すべてが「うまくいく」ことを知って、単にそれを拡張します。

テスト要件をすべて設定できます。

  • Springランナー - または他のランナーが必要な場合は、できればルール

後 ** プロファイル - 理想的には私たちの総計 __test __profile

  • 初期設定 - アプリケーションの状態を設定する

前の点を考慮に入れた単純な基本クラスを見てみましょう。

@SpringBootTest
@ActiveProfiles("test")
abstract class AbstractSpringIntegrationTest {

    @Rule
    @JvmField
    val springMethodRule = SpringMethodRule()

    companion object {
        @ClassRule
        @JvmField
        val SPRING__CLASS__RULE = SpringClassRule()
    }
}

4.7. 州管理

ユニットテストの「ユニット」がどこにあるかを覚えておくことが重要です。簡単に言うと、一貫した結果を得るためにいつでも単一のテスト(またはサブセット)を実行できるということです。

したがって、すべてのテストが開始される前に、状態は明確で既知である必要があります。

つまり、テストの結果は、それが単独で実行されたのか、他のテストと一緒に実行されたのかに関係なく、一貫性があるはずです。

この考え方は統合テストにも同じように当てはまります。新しいテストを開始する前に、アプリに既知の(そして繰り返し可能な)状態があることを確認する必要があります。

スピードアップのために再利用するコンポーネント(アプリケーションコンテキスト、DB、キュー、ファイルなど)が多ければ多いほど、州の汚染を受ける可能性が高くなります。

クラス継承を使用してオールインしたと仮定すると、今、状態を管理するための中心的な場所があります。

テストを実行する前に、抽象クラスを拡張して、アプリが既知の状態にあることを確認しましょう。

この例では、(さまざまなデータソースからの)複数のリポジトリと Wiremock サーバーがあると仮定します。

@SpringBootTest
@ActiveProfiles("test")
@AutoConfigureWireMock(port = 8666)
@AutoConfigureMockMvc
abstract class AbstractSpringIntegrationTest {

   //... spring rules are configured here, skipped for clarity

    @Autowired
    protected lateinit var wireMockServer: WireMockServer

    @Autowired
    lateinit var jdbcTemplate: JdbcTemplate

    @Autowired
    lateinit var repos: Set<MongoRepository<** , ** >>

    @Autowired
    lateinit var cacheManager: CacheManager

    @Before
    fun resetState() {
        cleanAllDatabases()
        cleanAllCaches()
        resetWiremockStatus()
    }

    fun cleanAllDatabases() {
        JdbcTestUtils.deleteFromTables(jdbcTemplate, "table1", "table2")
        jdbcTemplate.update("ALTER TABLE table1 ALTER COLUMN id RESTART WITH 1")
        repos.forEach { it.deleteAll() }
    }

    fun cleanAllCaches() {
        cacheManager.cacheNames
          .map { cacheManager.getCache(it) }
          .filterNotNull()
          .forEach { it.clear() }
    }

    fun resetWiremockStatus() {
        wireMockServer.resetAll()
       //set default requests if any
    }
}

4.8. 単体テストへのリファクタリング

これはおそらく最も重要な点の1つです。我々は実際に我々のアプリのいくつかの高レベルのポリシーを実行しているいくつかの統合テストで自分自身を見つけるでしょう。

  • いくつかの統合テストがコアビジネスロジックの多くのケースをテストしているのを見つけたときはいつでも、私たちのアプローチを再考してユニットテストに分解する時が来ました。**

これを成功させるためにここで考えられるパターンは次のとおりです。

  • コアの複数のシナリオをテストしている統合テストを特定する

ビジネスの論理 ** スイートを複製し、そのコピーをユニットTestsにリファクタリングします - これで

段階、私達はそれを作るためにも生産コードを分解する必要があるかもしれません テスト可能 ** すべてのテストを緑色にする

  • 統合において十分注目に値するハッピーパスのサンプル

スイート - リファクタリングまたは参加して、いくつかの形を変える必要があるかもしれません ** 残っている統合テストを削除する

Michael Feathersがこれを達成するための多くのテクニックをレガシーコードで効果的に機能させることでカバーします。

5.まとめ

この記事では、Springを中心とした統合テストについて紹介しました。

まず、統合テストの重要性と、それらがSpringアプリケーションに特に関連性がある理由について説明しました。

その後、Webアプリケーションの特定の種類の統合テストに役立つ可能性があるツールをいくつかまとめました。

最後に、テストの実行時間を遅くする可能性のある問題のリストとそれを改善するためのトリックを調べました。