Spring統合テストの最適化

Spring統合テストの最適化

1. 前書き

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

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

後で、ウェブアプリに焦点を当てた複数のシナリオについて説明します。

Next, we’ll discuss some strategies to improve testing speed、テストの形成方法とアプリ自体の形成方法の両方に影響を与える可能性のあるさまざまなアプローチについて学習します。

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

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

2. 統合テスト

Integration tests are a fundamental part of automated test suites.healthy test pyramidに従う場合、単体テストほど多くはないはずですが。 Springなどのフレームワークに依存しているため、システムの特定の動作のリスクを軽減するために、かなりの量の統合テストが必要になります。

The more we simplify our code by using Spring modules (data, security, social…), the bigger a need for integration tests. これは、インフラストラクチャのビットとボブを@Configurationクラスに移動するときに特に当てはまります。

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

統合テストは自信をつけるのに役立ちますが、代償が伴います:

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

  • また、統合テストはより広いテスト範囲を意味し、ほとんどの場合に理想的ではありません

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

3. Webアプリのテスト

SpringはWebアプリケーションをテストするためにいくつかのオプションを提供しますが、ほとんどのSpring開発者はそれらに精通しています。これらは次のとおりです。

  • MockMvc:サーブレットAPIをモックし、非リアクティブWebアプリに役立ちます

  • TestRestTemplate:アプリをポイントして使用でき、モックされたサーブレットが望ましくない非リアクティブWebアプリに役立ちます

  • WebTestClient:リアクティブなWebアプリのテストツールであり、要求/応答がモックされているか、実サーバーにアクセスしています。

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

さらに深く掘り下げたい場合は、お気軽にご覧ください。

4. 実行時間の最適化

統合テストは素晴らしいです。 彼らは私たちにかなりの自信を与えます。 また、適切に実装されていれば、モックやセットアップノイズが少なく、非常に明確な方法でアプリの意図を説明できます。

ただし、アプリが成熟して開発が積み重なると、ビルド時間が必然的に長くなります。 ビルド時間が長くなると、毎回すべてのテストを実行し続けることが非現実的になる場合があります。

その後、フィードバックループに影響を与え、開発のベストプラクティスを実現します。

さらに、統合テストは本質的に高価です。 ある種の永続性の開始、要求の送信(localhostを離れることがない場合でも)、または何らかのIOの実行には時間がかかります。

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

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

  • プロファイルの賢明な使用–プロファイルがパフォーマンスに与える影響

  • @MockBean – howモッキングヒットのパフォーマンスを再検討する

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

  • @DirtiesContext – aの有用で危険なアノテーションと、それを使用しない方法について慎重に検討する

  • テストスライスを使用する-支援する、または作業を進めることができるクールなツール

  • クラスの継承を使用する-安全な方法でテストを整理する方法

  • 状態管理–不安定なテストを回避するための良い習慣

  • 単体テストへのリファクタリング-堅牢でスナッピーなビルドを取得するための最良の方法

始めましょう!

4.1. プロファイルを賢く使用する

Profilesは非常に優れたツールです。 つまり、アプリの特定の領域を有効または無効にすることができるシンプルなタグ。 それらを使ってimplement feature flagsすることもできます!

プロファイルが豊富になるにつれて、統合テストで時々交換したくなるでしょう。 @ActiveProfilesのように、そうするための便利なツールがあります。 ただし、every time we pull a test with a new profile, a new ApplicationContext gets created.

アプリケーションコンテキストの作成は、バニラスプリングブートアプリには何も含まれていないため、簡単に実行できます。 ORMといくつかのモジュールを追加すると、すぐに7秒以上に急上昇します。

多数のプロファイルを追加し、それらをいくつかのテストに分散させると、60秒以上のビルドがすぐに得られます(ビルドの一部としてテストを実行すると仮定します。そうする必要があります)。

十分に複雑なアプリケーションに直面すると、これを修正するのは困難です。 ただし、事前に慎重に計画する場合、賢明なビルド時間を維持するのは簡単です。

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

  • 集約プロファイル、つまり test、必要なすべてのプロファイルを含める–どこでもテストプロファイルに固執する

  • テスト容易性を考慮してプロファイルを設計します。 プロファイルを切り替える必要がある場合、おそらくより良い方法があります

  • テストプロファイルを一元化された場所に記載します。これについては後で説明します

  • すべてのプロファイルの組み合わせをテストすることは避けてください。 または、特定のプロファイルセットでアプリをテストする環境ごとにe2eテストスイートを用意することもできます

4.2. @MockBeanの問題

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

Springの魔法が必要であるが、特定のコンポーネントをモックしたい場合は、@MockBeanが非常に便利です。 しかし、それは代償です。

Every time @MockBean appears in a class, the ApplicationContext cache gets marked as dirty, hence the runner will clean the cache after the test-class is done.これもビルドに秒数を追加します。

これは物議を醸すものですが、この特定のシナリオをあざけるのではなく、実際のアプリを実行しようとすると役立つ場合があります。 もちろん、ここには特効薬はありません。 依存関係を模倣することを許可しないと、境界がぼやけます。

私たちは考えるかもしれません:テストしたいのがRESTレイヤーだけなのに、なぜ持続するのでしょうか? これは公正な点であり、常に妥協点があります。

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

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

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

ユーザーを作成する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ブラックボックスとして処理してユーザーを送信するため、テストの境界に違反しますが、後で実装の詳細を使用してアサートします。つまり、ユーザーは一部のDBに保持されます。

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境界に関係のない副作用は認識されていません。 DBs

  • 最後に、私たちのテストは、システムの意図を明確に表現しています。POSTすると、ユーザーを取得できるようになります。

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

  • 「副作用」エンドポイントがない場合があります。ここでのオプションは、「テストエンドポイント」の作成を検討することです

  • 複雑さが高すぎてアプリ全体に当てはまらない:ここでのオプションは、スライスを検討することです(後で説明します)

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

テストでApplicationContextを変更する必要がある場合があります。 このシナリオでは、@DirtiesContextがまさにその機能を提供します。

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

Some misuses of @DirtiesContext include application cache reset or in memory DB resets.統合テストでこれらのシナリオを処理するためのより良い方法があります。いくつかについては、以降のセクションで説明します。

4.5. テストスライスの使用

テストスライスは、1.4で導入されたSpringBoot機能です。 アイデアはかなり単純です。Springは、アプリの特定のスライスに対して縮小されたアプリケーションコンテキストを作成します。

また、フレームワークは非常に最小限の設定を処理します。

Spring Bootには、すぐに使用できる適切な数のスライスがあり、独自のスライスも作成できます。

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

  • @DataJpaTest:利用可能なORMを含むJPABeanを登録します

  • @JdbcTest:生のJDBCテストに役立ち、ORMフリルなしでデータソースとメモリDBを処理します

  • @DataMongoTest:メモリ内のmongoテストセットアップを提供しようとします

  • @WebMvcTest:アプリの残りの部分がない模擬MVCテストスライス

  • …(the sourceをチェックしてすべてを見つけることができます)

この特定の機能を賢明に使用すれば、特に小規模/中規模のアプリのパフォーマンスに関して大きなペナルティなしで狭いテストを構築するのに役立ちます。

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

4.6. クラス継承の使用

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

If we provide a solid setup, our team will simply extend it, knowing that everything ‘just works'.このようにして、状態の管理やフレームワークの構成について心配する必要がなくなり、目前の問題に集中できます。

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

  • スプリングランナー–またはできればルール。後で他のランナーが必要になった場合に備えて

  • プロファイル–理想的には集約されたtest profile

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

前のポイントを処理する単純な基本クラスを見てみましょう。

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

    @Rule
    @JvmField
    val springMethodRule = SpringMethodRule()

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

4.7. 州管理

ユニットテストcomes fromの「ユニット」の場所を覚えておくことが重要です。 簡単に言うと、一貫した結果が得られる任意の時点で単一のテスト(またはサブセット)を実行できることを意味します。

したがって、すべてのテストを開始する前に、状態はクリーンで既知である必要があります。

言い換えると、テストの結果は、単独で実行されるか、他のテストと一緒に実行されるかに関係なく、一貫している必要があります。

この考え方は、統合テストにもまったく同じです。 新しいテストを開始する前に、アプリが既知の(および繰り返し可能な)状態であることを確認する必要があります。 物事を高速化するために再利用するコンポーネント(アプリコンテキスト、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>

    @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つです。 アプリの高レベルのポリシーを実際に実行している統合テストを何度も繰り返します。

コアビジネスロジックの多数のケースをテストする統合テストが見つかった場合は、アプローチを再考し、それらを単体テストに分解するときが来ました。

これを成功させるための可能なパターンは次のとおりです。

  • コアビジネスロジックの複数のシナリオをテストしている統合テストを特定する

  • スイートを複製し、そのコピーを単体テストにリファクタリングします。この段階では、テスト可能にするために本番コードも分解する必要があるかもしれません

  • すべてのテストをグリーンにする

  • 統合スイートで十分に注目に値するハッピーパスのサンプルを残してください。リファクタリングまたは参加して、いくつかの形状を変更する必要があるかもしれません

  • 残りの統合テストを削除します

Michael Feathersは、これを達成するための多くの手法と、レガシーコードを効果的に使用する方法について説明しています。

5. 概要

この記事では、Springを中心とした統合テストの概要を紹介しました。

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

その後、Web Appsの特定の種類の統合テストに役立つツールをまとめました。

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