現代のソフトウェア開発では、効果的なテストがアプリケーションの信頼性と安定性を確保する上で重要な役割を果たします。
この記事では、統合テストの作成に関する実用的な推奨事項を示し、外部サービスとのやり取りの仕様に焦点を当てて、テストの可読性と保守性を高める方法を説明します。このアプローチは、テストの効率を高めるだけでなく、アプリケーション内の統合プロセスに対する理解を深めることにも役立ちます。具体的な例を通して、DSL ラッパー、JsonAssert、Pact などのさまざまな戦略とツールを検討し、統合テストの品質と可視性を向上させるための包括的なガイドを読者に提供します。
この記事では、Spring アプリケーションでの HTTP 相互作用をテストするために Groovy の Spock Framework を使用して実行された統合テストの例を示します。同時に、提案された主なテクニックとアプローチは、HTTP 以外のさまざまな種類の相互作用に効果的に適用できます。
記事「Spring で効果的な統合テストを書く: HTTP リクエスト モッキングのための組織化されたテスト戦略」では、それぞれが特定の役割を果たす個別のステージに明確に分けたテストの記述方法について説明しています。これらの推奨事項に従って、1 つのリクエストではなく 2 つのリクエストをモックするテスト例を説明しましょう。簡潔にするために、Act ステージ (実行) は省略します (完全なテスト例はプロジェクト リポジトリにあります)。
提示されたコードは、条件に応じて「サポート コード」(灰色) と「外部インタラクションの仕様」(青色) の部分に分かれています。サポート コードには、要求のインターセプトや応答のエミュレーションなど、テスト用のメカニズムとユーティリティが含まれています。外部インタラクションの仕様では、予想される要求と応答など、テスト中にシステムが対話する必要がある外部サービスに関する特定のデータが記述されています。サポート コードはテストの基礎を築きますが、仕様は、テストしようとしているシステムのビジネス ロジックと主要な機能に直接関連しています。
仕様はコードの小さな部分を占めますが、テストを理解する上で大きな価値を持ちます。一方、サポート コードは大きな部分を占めますが、価値は少なく、各モック宣言ごとに繰り返し使用されます。このコードは、MockRestServiceServer で使用することを目的としています。WireMock の例を参照すると、同じパターンが見られます。仕様はほぼ同じですが、サポート コードは異なります。
この記事の目的は、仕様に重点を置き、サポートコードを後回しにしてテストを作成するための実用的な推奨事項を提供することです。
私たちのテスト シナリオでは、リクエストを OpenAI API に転送し、ユーザーに応答を返す仮想の Telegram ボットを提案します。
サービスとやりとりするためのコントラクトは、操作の主なロジックを強調するために簡略化して記述されています。以下は、アプリケーション アーキテクチャを示すシーケンス図です。この設計はシステム アーキテクチャの観点から疑問が生じる可能性があると理解していますが、理解した上で取り組んでください。ここでの主な目標は、テストでの可視性を高める方法を示すことです。
この記事では、テストの作成に関する次の実用的な推奨事項について説明します。
DSL ラッパーを使用すると、定型的なモック コードを非表示にし、仕様を操作するためのシンプルなインターフェイスを提供できます。ここで提案されているのは特定の DSL ではなく、それが実装する一般的なアプローチであることを強調することが重要です。DSL を使用した修正されたテスト例を以下に示します ( 完全なテスト テキスト)。
setup: def openaiRequestCaptor = restExpectation.openai.completions(withSuccess("{...}")) def telegramRequestCaptor = restExpectation.telegram.sendMessage(withSuccess("{}")) when: ... then: openaiRequestCaptor.times == 1 telegramRequestCaptor.times == 1
たとえば、メソッドrestExpectation.openai.completions
は次のように記述されます。
public interface OpenaiMock { /** * This method configures the mock request to the following URL: {@code https://api.openai.com/v1/chat/completions} */ RequestCaptor completions(DefaultResponseCreator responseCreator); }
メソッドにコメントを付けると、コード エディターでメソッド名にマウスを移動したときに、モックされる URL を表示するなどのヘルプが表示されます。
提案された実装では、モックからの応答の宣言はResponseCreator
インスタンスを使用して行われ、次のようなカスタムのものが可能になります。
public static ResponseCreator withResourceAccessException() { return (request) -> { throw new ResourceAccessException("Error"); }; }
応答セットを指定した失敗シナリオのテスト例を以下に示します。
import static org.springframework.http.HttpStatus.FORBIDDEN setup: def openaiRequestCaptor = restExpectation.openai.completions(openaiResponse) def telegramRequestCaptor = restExpectation.telegram.sendMessage(withSuccess("{}")) when: ... then: openaiRequestCaptor.times == 1 telegramRequestCaptor.times == 0 where: openaiResponse | _ withResourceAccessException() | _ withStatus(FORBIDDEN) | _
WireMock の場合、レスポンスの構成が若干異なる点 ( テスト コード、 レスポンス ファクトリ クラス コード) を除いて、すべてが同じです。
DSL を実装する場合、メソッド パラメータに@Language("JSON")
の注釈を付けて、IntelliJ IDEA の特定のコード スニペットの言語機能サポートを有効にすることができます。たとえば、JSON を使用すると、エディターは文字列パラメータを JSON コードとして扱い、構文の強調表示、自動補完、エラー チェック、ナビゲーション、構造検索などの機能を有効にします。注釈の使用例を次に示します。
public static DefaultResponseCreator withSuccess(@Language("JSON") String body) { return MockRestResponseCreators.withSuccess(body, APPLICATION_JSON); }
エディターでの表示は次のようになります。
JSONAssert ライブラリは、JSON 構造のテストを簡素化するように設計されています。さまざまな比較モードをサポートし、開発者が期待される JSON 文字列と実際の JSON 文字列を高い柔軟性で簡単に比較できるようにします。
これにより、次のような検証の説明から移動できるようになります
openaiRequestCaptor.body.model == "gpt-3.5-turbo" openaiRequestCaptor.body.messages.size() == 1 openaiRequestCaptor.body.messages[0].role == "user" openaiRequestCaptor.body.messages[0].content == "Hello!"
このようなものに
assertEquals("""{ "model": "gpt-3.5-turbo", "messages": [{ "role": "user", "content": "Hello!" }] }""", openaiRequestCaptor.bodyString, false)
私の意見では、2 番目のアプローチの主な利点は、ドキュメント、ログ、テストなど、さまざまなコンテキストにわたってデータ表現の一貫性が確保されることです。これにより、テスト プロセスが大幅に簡素化され、比較の柔軟性とエラー診断の精度が向上します。したがって、テストの作成と保守にかかる時間を節約できるだけでなく、テストの読みやすさと情報量も向上します。
Spring Boot 内で作業する場合、少なくともバージョン 2 以降では、 org.springframework.boot:spring-boot-starter-test
にはすでにorg.skyscreamer:jsonassert
への依存関係が含まれているため、ライブラリを操作するために追加の依存関係は必要ありません。
1 つの観察結果は、JSON 文字列がテストのかなりの部分を占めているということです。これらを非表示にすべきでしょうか? はい、いいえ。どちらがより多くのメリットをもたらすかを理解することが重要です。これらを非表示にすると、テストがよりコンパクトになり、一目でテストの本質を把握しやすくなります。一方、徹底的な分析を行うには、外部インタラクションの仕様に関する重要な情報の一部が非表示になり、ファイル間で余分なジャンプが必要になります。決定は利便性に依存します。より快適な方法を選択してください。
JSON 文字列をファイルに保存する場合、応答とリクエストを JSON ファイルに別々に保存するという簡単な方法があります。以下は実装オプションを示すテスト コード ( 完全版) です。
setup: def openaiRequestCaptor = restExpectation.openai.completions(withSuccess(fromFile("json/openai/response.json"))) def telegramRequestCaptor = restExpectation.telegram.sendMessage(withSuccess("{}")) when: ... then: openaiRequestCaptor.times == 1 telegramRequestCaptor.times == 1
fromFile
メソッドはsrc/test/resources
ディレクトリ内のファイルから文字列を読み取るだけであり、革新的なアイデアはありませんが、プロジェクト リポジトリで参照用に引き続き使用できます。
文字列の変数部分については、 org.apache.commons.text.StringSubstitutorによる置換を使用し、モックを記述するときに値のセットを渡すことをお勧めします。次に例を示します。
setup: def openaiRequestCaptor = restExpectation.openai.completions(withSuccess(fromFile("json/openai/response.json", [content: "Hello! How can I assist you today?"])))
JSON ファイル内の置換部分は次のようになります。
... "message": { "role": "assistant", "content": "${content:-Hello there, how may I assist you today?}" }, ...
ファイル ストレージ アプローチを採用する場合の開発者にとっての唯一の課題は、テスト リソースでの適切なファイル配置スキームと命名スキームを開発することです。間違いを犯しやすく、これらのファイルでの作業エクスペリエンスが悪化する可能性があります。この問題の解決策の 1 つは、後で説明する Pact などの仕様を使用することです。
Groovy で記述されたテストで説明したアプローチを使用する場合、不便に遭遇する可能性があります。IntelliJ IDEA ではコードからファイルへの移動はサポートされていませんが、この機能のサポートは将来追加される予定です。Java で記述されたテストでは、これはうまく機能します。
まずは用語から始めましょう。
契約テストは、各アプリケーションを個別にテストして、送受信するメッセージが「契約」に文書化された相互理解に準拠していることを確認する統合ポイントのテスト方法です。このアプローチにより、システムのさまざまな部分間の相互作用が期待どおりであることが保証されます。
契約テストのコンテキストにおける契約とは、アプリケーション間で交換されるメッセージ (要求と応答) の形式と構造に関する合意を記録したドキュメントまたは仕様です。これは、統合において各アプリケーションが他のアプリケーションによって送受信されるデータを正しく処理できることを確認するための基礎として機能します。
契約は、消費者 (たとえば、データを取得したいクライアント) とプロバイダー (たとえば、クライアントが必要とするデータを提供するサーバー上の API) の間で確立されます。
コンシューマー主導テストは、コンシューマーが自動テストの実行中にコントラクトを生成するコントラクト テストのアプローチです。これらのコントラクトはプロバイダーに渡され、プロバイダーは一連の自動テストを実行します。コントラクト ファイルに含まれる各リクエストはプロバイダーに送信され、受信した応答はコントラクト ファイルで指定された期待応答と比較されます。両方の応答が一致する場合、コンシューマーとサービス プロバイダーは互換性があることを意味します。
最後に、Pact です。Pact は、コンシューマー主導の契約テストのアイデアを実装するツールです。コードファーストのテスト開発に重点を置き、HTTP 統合とメッセージベースの統合の両方のテストをサポートします。
先ほど述べたように、このタスクには Pact の契約仕様とツールを使用できます。実装は次のようになります ( 完全なテスト コード):
setup: def openaiRequestCaptor = restExpectation.openai.completions(fromContract("openai/SuccessfulCompletion-Hello.json")) def telegramRequestCaptor = restExpectation.telegram.sendMessage(withSuccess("{}")) when: ... then: openaiRequestCaptor.times == 1 telegramRequestCaptor.times == 1
契約ファイルは確認可能です。
契約ファイルを使用する利点は、リクエストとレスポンスの本文だけでなく、リクエスト パス、ヘッダー、HTTP レスポンス ステータスなど、外部インタラクション仕様の他の要素も含まれているため、そのような契約に基づいてモックを完全に記述できることです。
この場合、契約テストに限定されており、消費者主導のテストには拡張されていないことに注意してください。ただし、Pact をさらに詳しく調べたい場合は、この方法をお勧めします。
この記事では、Spring Framework を使用した開発のコンテキストで統合テストの可視性と効率性を高めるための実用的な推奨事項について説明しました。私の目標は、外部インタラクションの仕様を明確に定義し、定型コードを最小限に抑えることの重要性に焦点を当てることでした。この目標を達成するために、DSL ラッパーと JsonAssert の使用、JSON ファイルへの仕様の保存、Pact による契約の操作を提案しました。この記事で説明したアプローチは、テストの作成と保守のプロセスを簡素化し、テストの読みやすさを向上させ、そして最も重要なことに、システム コンポーネント間のインタラクションを正確に反映することでテスト自体の品質を高めることを目的としています。
テストをデモンストレーションするプロジェクト リポジトリへのリンク - sandbox/bot 。
この記事にご注目いただきありがとうございます。効果的で目に見えるテストの作成に向けて頑張ってください。