Spring Cloud Contract の導入

Spring Cloud Contract は、TDD をソフトウェアアーキテクチャのレベルにプルアップます。これにより、コンシューマー主導およびプロデューサー主導の契約テストを実行できます。

ヒストリー

Spring Cloud Contract になる前は、このプロジェクトは最も正確な [GitHub] (英語) と呼ばれていました。( Codearte [GitHub] (英語) ) のマルチングシェジザク (英語) ヤクブクブリンスキー (英語) によって作成されました。

0.1.0 リリースは 2015 年 1 月 26 日に行われ、2016 年 2 月 29 日の 1.0.0 リリースで安定しました。

なぜそれが必要なのですか?

次の図に示すように、複数のマイクロサービスで構成されるシステムがあると仮定します。

Microservices Architecture

テストの課題

前のセクションのイメージの左上隅にあるアプリケーションをテストして、他のサービスと通信できるかどうかを確認したい場合は、次の 2 つのいずれかを実行できます。

  • すべてのマイクロサービスをデプロイし、エンドツーエンドのテストを実行します。

  • 単体テストおよび統合テストで他のマイクロサービスをモックします。

どちらにも利点がありますが、多くの欠点もあります。

すべてのマイクロサービスをデプロイし、エンドツーエンドのテストを実行する

利点:

  • 本番をシミュレートします。

  • サービス間の実際の通信をテストします。

短所:

  • 1 つのマイクロサービスをテストするには、6 つのマイクロサービス、いくつかのデータベース、その他の項目をデプロイする必要があります。

  • テストが実行される環境は、単一のテストスイートに対してロックされます (その間、他の誰もテストを実行できなくなります)。

  • 実行には長い時間がかかります。

  • フィードバックはプロセスの非常に遅い段階で得られます。

  • これらはデバッグが非常に困難です。

単体テストおよび統合テストで他のマイクロサービスをモックする

利点:

  • 彼らは非常に迅速なフィードバックを提供します。

  • インフラストラクチャ要件はありません。

短所:

  • サービスの実装者は、現実とは何の関係もない可能性のあるスタブを作成します。

  • テストに合格しても本番環境に失敗しても、本番環境に移行できます。

上記の課題を解決するために開発されたのが Spring Cloud Contract です。主なアイデアは、マイクロサービス全体をセットアップする必要なく、非常に迅速なフィードバックを提供することです。スタブで作業する場合、必要なアプリケーションは、アプリケーションが直接使用するアプリケーションだけです。次の図は、スタブとアプリケーションの関連を示しています。

Stubbed Services

Spring Cloud Contract を使用すると、使用するスタブが呼び出したサービスによって作成されたという確信が得られます。また、使えるということは、製作者側でテストされたということになります。つまり、これらのスタブは信頼できます。

目的

Spring Cloud Contract の主な目的は次のとおりです。

  • HTTP およびメッセージングスタブ (クライアントの開発時に使用される) が実際のサーバー側の実装とまったく同じことを行うようにするため。

  • ATDD (受け入れテスト駆動開発) 手法とマイクロサービスアーキテクチャスタイルを推進します。

  • 契約の変更を公開し、双方ですぐに確認できる方法を提供します。

  • サーバー側で使用するボイラープレートテストコードを生成します。

デフォルトでは、Spring Cloud Contract は HTTP サーバースタブとしてワイヤーモック (英語) と統合されます。

Spring Cloud Contract の目的は、契約書にビジネス機能を書き始めることではありません。不正チェックのビジネスユースケースがあると仮定します。ユーザーが 100 の異なる理由で詐欺師になる可能性がある場合、肯定的な場合と否定的な場合の 2 つの契約を作成すると想定します。契約 テストは、完全な動作をシミュレートするためではなく、アプリケーション間の契約をテストするために使用されます。

契約とは何ですか ?

サービスの利用者として、正確に何を達成したいのかを定義する必要があります。期待を明確にする必要があります。こそ契約書を書くのです。言い換えれば、契約は、API またはメッセージ通信がどのようにあるべきかについての合意です。次の例を考えてみましょう。

クライアント企業の ID と当社からの借入希望額を含むリクエストを送信するとします。また、PUT メソッドを使用して、これを /fraudcheck URL に送信したいと考えています。次のリストは、Groovy と YAML の両方でクライアントを詐欺としてマークする必要があるかどうかをチェックする契約を示しています。

  • groovy

org.springframework.cloud.contract.spec.Contract.make {
	request { // (1)
		method 'PUT' // (2)
		url '/fraudcheck' // (3)
		body([ // (4)
			   "client.id": $(regex('[0-9]{10}')),
			   loanAmount : 99999
		])
		headers { // (5)
			contentType('application/json')
		}
	}
	response { // (6)
		status OK() // (7)
		body([ // (8)
			   fraudCheckStatus  : "FRAUD",
			   "rejection.reason": "Amount too high"
		])
		headers { // (9)
			contentType('application/json')
		}
	}
}

/*
From the Consumer perspective, when shooting a request in the integration test:

(1) - If the consumer sends a request
(2) - With the "PUT" method
(3) - to the URL "/fraudcheck"
(4) - with the JSON body that
 * has a field `client.id` that matches a regular expression `[0-9]{10}`
 * has a field `loanAmount` that is equal to `99999`
(5) - with header `Content-Type` equal to `application/json`
(6) - then the response will be sent with
(7) - status equal `200`
(8) - and JSON body equal to
 { "fraudCheckStatus": "FRAUD", "rejectionReason": "Amount too high" }
(9) - with header `Content-Type` equal to `application/json`

From the Producer perspective, in the autogenerated producer-side test:

(1) - A request will be sent to the producer
(2) - With the "PUT" method
(3) - to the URL "/fraudcheck"
(4) - with the JSON body that
 * has a field `client.id` that will have a generated value that matches a regular expression `[0-9]{10}`
 * has a field `loanAmount` that is equal to `99999`
(5) - with header `Content-Type` equal to `application/json`
(6) - then the test will assert if the response has been sent with
(7) - status equal `200`
(8) - and JSON body equal to
 { "fraudCheckStatus": "FRAUD", "rejectionReason": "Amount too high" }
(9) - with header `Content-Type` matching `application/json.*`
 */
yaml
request: # (1)
  method: PUT # (2)
  url: /yamlfraudcheck # (3)
  body: # (4)
    "client.id": 1234567890
    loanAmount: 99999
  headers: # (5)
    Content-Type: application/json
  matchers:
    body:
      - path: $.['client.id'] # (6)
        type: by_regex
        value: "[0-9]{10}"
response: # (7)
  status: 200 # (8)
  body:  # (9)
    fraudCheckStatus: "FRAUD"
    "rejection.reason": "Amount too high"
  headers: # (10)
    Content-Type: application/json


#From the Consumer perspective, when shooting a request in the integration test:
#
#(1) - If the consumer sends a request
#(2) - With the "PUT" method
#(3) - to the URL "/yamlfraudcheck"
#(4) - with the JSON body that
# * has a field `client.id`
# * has a field `loanAmount` that is equal to `99999`
#(5) - with header `Content-Type` equal to `application/json`
#(6) - and a `client.id` json entry matches the regular expression `[0-9]{10}`
#(7) - then the response will be sent with
#(8) - status equal `200`
#(9) - and JSON body equal to
# { "fraudCheckStatus": "FRAUD", "rejectionReason": "Amount too high" }
#(10) - with header `Content-Type` equal to `application/json`
#
#From the Producer perspective, in the autogenerated producer-side test:
#
#(1) - A request will be sent to the producer
#(2) - With the "PUT" method
#(3) - to the URL "/yamlfraudcheck"
#(4) - with the JSON body that
# * has a field `client.id` `1234567890`
# * has a field `loanAmount` that is equal to `99999`
#(5) - with header `Content-Type` equal to `application/json`
#(7) - then the test will assert if the response has been sent with
#(8) - status equal `200`
#(9) - and JSON body equal to
# { "fraudCheckStatus": "FRAUD", "rejectionReason": "Amount too high" }
#(10) - with header `Content-Type` equal to `application/json`
契約は信頼できるソースから得られることが期待されます。信頼できない場所からの契約をダウンロードしたり、操作したりしないでください。