1. 序章

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

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

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

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

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

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

2. 統合テスト

統合テストは自動テストスイートの基本的な部分です。正常なテストピラミッドに従う場合、単体テストほど多くはないはずですが。 Springなどのフレームワークに依存すると、システムの特定の動作のリスクを軽減するために、かなりの量の統合テストが必要になります。

Springモジュール(データ、セキュリティ、ソーシャルなど)を使用してコードを単純化すればするほど、統合テストの必要性が高まります。 これは、インフラストラクチャの一部を次の場所に移動するときに特に当てはまります。 @構成クラス。

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

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

  • これは実行速度が遅くなります。つまり、ビルドが遅くなります。
  • また、統合テストは、ほとんどの場合理想的ではない、より広いテスト範囲を意味します

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

3. Webアプリのテスト

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

  • MockMvc :サーブレットAPIをモックし、非反応性のWebアプリに役立ちます
  • TestRestTemplate :私たちのアプリを指すように使用でき、モックされたサーブレットが望ましくない非反応性のWebアプリに役立ちます
  • WebTestClient :モックされたリクエスト/レスポンスまたは実サーバーへのアクセスの両方を備えたリアクティブWebアプリのテストツールです

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

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

4. 実行時間の最適化

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

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

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

さらに、統合テストは本質的に費用がかかります。 ある種の永続性の起動、リクエストの送信( localhost を離れることがない場合でも)、またはIOの実行には時間がかかります。

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

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

  • プロファイルを賢く使用する–プロファイルがパフォーマンスに与える影響
  • @MockBeanの再検討–モッキングがパフォーマンスに与える影響
  • @MockBean のリファクタリング–パフォーマンスを向上させるための代替手段
  • @ DirtiesContext – 有用だが危険なアノテーションと、それを使用しない方法について慎重に考える
  • テストスライスの使用–私たちの道を助けたり、進んだりすることができるクールなツール
  • クラス継承の使用–テストを安全な方法で整理する方法
  • 状態管理–フレークテストを回避するためのグッドプラクティス
  • 単体テストへのリファクタリング–堅実でスッキリとしたビルドを実現するための最良の方法

始めましょう!

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

プロファイルは非常に優れたツールです。 つまり、アプリの特定の領域を有効または無効にできる単純なタグです。 それらを使用して機能フラグを実装することもできます。

私たちのプロファイルがより豊かになるにつれて、統合テストで時々交換したくなるでしょう。 @ActiveProfiles のように、そうするための便利なツールがあります。 ただし、新しいプロファイルでテストをプルするたびに、新しいApplicationContextが作成されます。

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

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

十分に複雑なアプリケーションに直面すると、これを修正するのは困難です。 ただし、事前に慎重に計画を立てておけば、適切なビルド時間を維持することは簡単になります。

統合テストのプロファイルに関しては、覚えておくべきいくつかの秘訣があります。

  • 集約プロファイルを作成します。 test 、必要なすべてのプロファイルを含める–どこでもテストプロファイルに固執する
  • テスト容易性を念頭に置いてプロファイルを設計します。 プロファイルを切り替える必要が生じた場合は、おそらくより良い方法があります
  • テストプロファイルを一元化された場所に記述します–これについては後で説明します
  • すべてのプロファイルの組み合わせをテストすることは避けてください。 または、環境ごとにe2eテストスイートを使用して、その特定のプロファイルセットを使用してアプリをテストすることもできます。

4.2. @MockBeanの問題

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

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

@MockBeanがクラスに表示されるたびに、ApplicationContextキャッシュはダーティとしてマークされるため、テストクラスが完了した後、ランナーはキャッシュをクリーンアップします。これにより、ビルドにさらに数秒が追加されます。

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

テストしたいのが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境界に関係のない副作用は認識されていません。 DB
  • 最後に、私たちのテストは、システムの意図を明確に表現しています。POSTを実行すると、ユーザーを取得できるようになります。

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

  • 「副作用」エンドポイントがない可能性があります。ここでのオプションは、「テストエンドポイント」の作成を検討することです。
  • 複雑さが高すぎてアプリ全体に影響を与えることはできません。ここでのオプションは、スライスを検討することです(後で説明します)

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

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

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

@DirtiesContextの誤用には、アプリケーションキャッシュのリセットやメモリ内のDBのリセットなどがあります。統合テストでこれらのシナリオを処理するためのより良い方法があります。これについては、以降のセクションで説明します。

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

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

また、フレームワークは最小限の構成を処理します。

Spring Bootには、箱から出してすぐに利用できるかなりの数のスライスがあり、独自のスライスを作成することもできます。

  • @JsonTest:JSON関連コンポーネントを登録します
  • @DataJpaTest :利用可能なORMを含むJPABeanを登録します
  • @JdbcTest :生のJDBCテストに役立ち、ORMフリルなしでデータソースとメモリDBを処理します
  • @DataMongoTest :メモリ内のmongoテストセットアップを提供しようとします
  • @WebMvcTest :アプリの残りの部分を含まない模擬MVCテストスライス
  • …(ソースをチェックしてすべてを見つけることができます)

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

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

4.6. クラス継承の使用

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

しっかりとしたセットアップを提供すれば、チームはそれを拡張するだけで、すべてが「正常に機能する」ことを認識します。これにより、状態の管理やフレームワークの構成について心配する必要がなくなり、目前の問題に集中できます。

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

  • 春のランナー–または、後で他のランナーが必要になった場合に備えて、ルールが望ましい
  • プロファイル–理想的には集約テストプロファイル
  • 初期設定–アプリケーションの状態を設定します

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

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

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

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

  • コアビジネスロジックの複数のシナリオをテストしている統合テストを特定する
  • スイートを複製し、コピーを単体テストにリファクタリングします。この段階で、テスト可能にするために本番コードも分解する必要がある場合があります。
  • すべてのテストをグリーンにする
  • 統合スイートで十分に注目に値するハッピーパスサンプルを残してください-リファクタリングまたは結合して、いくつかを再形成する必要があるかもしれません
  • 残りの統合テストを削除します

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

5. 概要

この記事では、Springに焦点を当てた統合テストの概要を説明しました。

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

その後、Webアプリでの特定のタイプの統合テストに役立つ可能性のあるいくつかのツールを要約しました。

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