しおしお

IntelliJ IDEAのことなんかを書いてます

Spring Cloud Contractのstubをもとにサーバを起動してみる

Spring Cloud Contractを試してみた - しおしおの続きで、Producer側で生成したstubsをサーバとして起動してエンドポイントを叩いて結果が取得できるか試してみました。

Spring Cloud Contractを適用したプロジェクトの作成

新規でプロジェクトを作るのは大変なので、Spring Cloud Contractを試してみた - しおしおで作ったプロジェクトを使っていきたいと思います。

プロジェクトは、GitHubにいるので↓でcloneしてきます。

git clone git@github.com:siosio/spring-cloud-contract-example.git

Producer側でstubを生成する

generateClientStubsタスクを実行することで、stubを生成できます。 生成されたファイルは、build/stubs:/contracts/stubs配下にあります。

サンプルプロジェクトの場合、プロジェクト直下で↓を実行することで生成できます。

./gradlew server-service:generateClientStubs

生成したstubをもとにサーバを起動する

springcloud/spring-cloud-contract-stub-runnerイメージを使って、コンテナを起動できます。

環境変数に渡す値は、基本はAutoConfigureStubRunnerと同じ値になります。 大きく異なるのは、生成したstubをもとにサーバを起動したいので、STUBRUNNER_REPOSITORY_ROOTにはローカルのディレクトリを指定します。 指定した、ディレクトリには生成したstubが格納されているディレクトリの内容をコピーしておきます。

docker run  --rm \
  -e "STUBRUNNER_IDS=siosio:server-service:1.0.0:8080" \
  -e "STUBRUNNER_REPOSITORY_ROOT=stubs://file:///contracts/stubs/" \
  -e "STUBRUNNER_STUBS_MODE=LOCAL" \
  -p "8080:8080" \
  -v `pwd`/server-service/build/stubs:/contracts/stubs \
  springcloud/spring-cloud-contract-stub-runner:2.2.5.RELEASE

stubベースのサーバにアクセスしてみる

CDCの内容

CDCの内容的に、/sample/{数値}のエンドポイントを叩けば、stubが結果を返してくれるはずです。

import org.springframework.cloud.contract.spec.Contract

Contract.make {
  priority 2
  request {
    method('GET')
    url "/sample/${regex('\\d+')}"
  }

  response {
    status(OK())

    headers {
      contentType(applicationJson())
    }
    
    body(
        id: fromRequest().path(1),
        name: "name_${fromRequest().path(1)}"
    )
  }
}

実行結果

CDCの契約に従って、サーバが結果を返してくれていることが確認できますね。

$ curl http://localhost:8080/sample/1
{"id":"1","name":"name_1"}
$ curl http://localhost:8080/sample/123
{"id":"123","name":"name_123"}

これで、Producer側の開発が終わっていないタイミングでも、早いタイミングでインタフェースを確定してしまえば、stubベースのサーバを使ったConsumer側の開発ができそうな感じがありますね。

single-spaでマイクロフロントエンドしてみる

single-spa | single-spaなるマイクロフロントエンドさくっと作れそうなフレームワークを試してみました。

準備

single-spa はプロジェクト作成用などのcliを用意してくれているようなので、まずはそれをインストールします。

npm install --global create-single-spa

こんな感じにバージョン確認できればインストールは成功です。

create-single-spa --version
2.1.1

アプリケーションの作成手順

ここから新規で、single-spaを使ったマイクロフロントエンドなアプリケーションを作る流れになります。

ルートHTMLを持つプロジェクトの作成

create-single-spaコマンドを使って、moduleTyperoot-configを指定することでルートHTMLを持つプロジェクトを作成できます。 いくつか作成するプロジェクトに関する質問をされますが、好きなものを選ぶな&入力する感じでOKです。

create-single-spa --dir root-config --moduleType root-config

プロジェクトが作成できたら、早速起動してみましょう。

cd root-config
npm run start

ブラウザで、http://localhost:9000にアクセスしてこんな感じのページが表示されればOKですね! f:id:sioiri:20210228063528p:plain

1つめのマイクロフロントエンドとなるアプリケーションの作成

マイクルフロントエンドとなるアプリケーションも、create-single-spaを使って作成できます。 frameworkには、色々なもの*1が指定できますが、今回はvueを指定して2系でプロジェクトを作ってみます。 vueを指定した場合、vue-cliが実行されプロジェクトが作成されます。

create-single-spa --dir vue2-app --framework vue

さっそくプロジェクトを起動してみましょう。

cd vue2-app
npm run serve

ブラウザでhttp://localhost:8080にアクセスすると、いつものVueなアプリケーションの画面が表示されませんね… これは、run serveで起動した場合、結合モードでマイクロフロントエンド側のアプリケーションとして実行されていて単体での画面表示ができないことが原因みたいですね。 f:id:sioiri:20210228064605p:plain

マイクロフロントエンド側のアプリケーション単体で画面表示&開発をしたい場合にはserve:standaloneで起動してあげます。

npm run serve:standalone

これで再度ブラウザで表示してみるといつもの見慣れた初期画面が表示されますね。 f:id:sioiri:20210228065025p:plain

vue2のアプリケーションであることがさくっとわかるように、src/App.vueのtemplateを書き換えておきます。

<template>
  <div id="app">
    <h1>vue2 app</h1>
    <router-view/>
  </div>
</template>

ポートかぶりをなくすために、vue.config.jsを作成して、使用するポートも変更しておきます。

module.exports = {
    devServer: {
        port: 9001,
        disableHostCheck: true
    }
}

2つめのマイクロフロントエンドとなるアプリケーションの作成

1つ目と同じようにcreate-single-spaを使って作成していきます。2つ目のサービスはVueの3系を指定して作ってみます。

create-single-spa --dir vue3-app --framework vue

以降の手順は、1つ目のプロジェクトと同じため省略します。ポートは連番でわかりやすく9002に変更しておきます。

作成した2つのサービスを使って画面表示してみる

最初に作った、root-config側を編集して、2つのマイクロサービスを切り替えて画面表示していきます。

cd root-config
vi src/index.ejs

@single-spa/welcomeが記述されている行を探し、importmapを変更していきます。 importmapの名前には、各マイクロフロントエンドサービスのpackage.jsonに指定されている名前を指定するのがわかりやすいでしょう。 JavaScriptのパスは、マイクロフロントエンドサービスをnpm run serveで起動した際に画面に表示されているパス(下の画像の赤枠のパス)を指定します。 f:id:sioiri:20210228070846p:plain

  • 変更前
  <% if (isLocal) { %>
  <script type="systemjs-importmap">
    {
      "imports": {
        "@single-spa/welcome": "https://unpkg.com/single-spa-welcome/dist/single-spa-welcome.js",
        "@siosio/root-config": "//localhost:9000/siosio-root-config.js"
      }
    }
  </script>
  <% } %>
  • 変更後
  <% if (isLocal) { %>
  <script type="systemjs-importmap">
    {
      "imports": {
        "@siosio/root-config": "//localhost:9000/siosio-root-config.js",
        "@siosio/vue2-app": "http://localhost:9001/js/app.js",
        "@siosio/vue3-app": "http://localhost:9002/js/app.js"
      }
    }
  </script>
  <% } %>

続いて、各マイクロフロントエンドサービスの画面を表示する部分を変更しています。 変更する部分は、route defaultが記述されているあたりになります。 routepathを指定することでパスによって表示するマイクロフロントエンドサービスを切り替えられるようです。 applicationnameには、上で編集したimportmapの名前を指定してあげます。

  • 変更前
      <main>
        <route default>
          <application name="@single-spa/welcome"></application>
        </route>
      </main>
  • 変更後
      <main>
        <route path="/vue2">
          <application name="@siosio/vue2-app"></application>
        </route>
        <route path="/vue3">
          <application name="@siosio/vue3-app"></application>
        </route>
      </main>

アプリケーションを起動して画面表示をしてみましょう。

npm run start

ブラウザで、http://localhost:9000/vue2/http://localhost:9000/vue3で、それぞれのマイクロフロントエンドサービス側の画面が表示されれば成功ですね。

おわり。

Testcontainersのコンテナを複数テストクラスで使い回す

Testcontainersのコンテナを複数テストクラスで共有して使い回す方法になります。 コンテナを使い回すことで、テストクラスごとにコンテナが起動されテストの実行がめちゃ遅くなってしまう問題を解消することが期待できます。

コンテナをマニュアル起動するクラスを作成する

コンテナを手動起動するようなクラスを抽象クラスとして作ってあげます。

abstract class MySqlContainer {
    companion object {
        val mySqlContainer = MySQLContainer<Nothing>("mysql:5.7.32")
        init {
            mySqlContainer.start()
        }
    }
}

テストコード

コンテナを起動するクラスを親クラスに指定することで、コンテナが一度だけ起動するようになります。 Spring Bootのテストでは、@DynamicPropertySourceを使って、親クラスで起動したコンテナの情報をもとにデータベースの接続先情報などを上書きしてあげます。

@TestConstructor(autowireMode = TestConstructor.AutowireMode.ALL)
@SpringBootTest
@AutoConfigureMockMvc
internal class UserHandlerTest(
        val mockMvc: MockMvc,
        val dataSource: DataSource
): MySqlContainer() {
    companion object {

        @DynamicPropertySource
        @JvmStatic
        fun changeProperty(registry: DynamicPropertyRegistry): Unit {
            val mySqlContainer = MySqlContainer.mySqlContainer
            registry.add("spring.datasource.url", mySqlContainer::getJdbcUrl)
            registry.add("spring.datasource.username", mySqlContainer::getUsername)
            registry.add("spring.datasource.password", mySqlContainer::getPassword)
        }
    }

        val table = Table(dataSource, "user")
        val changes = Changes(table)
        changes.setStartPointNow()

        mockMvc.post("/api/users") {
            contentType = MediaType.APPLICATION_JSON
            content = """{"name": "siosio"}"""
        }.andExpect {
            status { isOk }
        }

        changes.setEndPointNow()
        Assertions.assertThat(changes)
                .hasNumberOfChanges(1)
                .changeOfCreation()
                .rowAtEndPoint().value("name").isEqualTo("siosio")
    }
}

テストクラスの親クラスに指定できない場合…

すでに親クラスを指定済みで、そこに手を入れられないようなケースにはテストフレームワーク側の拡張機能を使って対応できます。 例えば、JUnit5の場合は以下のような拡張実装を作ることで対応できます。

class MySqlContainer: Extension {
    companion object {
        val mySqlContainer = MySQLContainer<Nothing>("mysql:5.7.32")
        init {
            mySqlContainer.start()
        }
    }
}

テストコード側では、@RegisterExtensionで拡張実装を登録し利用できます。

    companion object {
        @JvmField
        @RegisterExtension
        val mySqlContainer = MySqlContainer()

        @DynamicPropertySource
        @JvmStatic
        fun changeProperty(registry: DynamicPropertyRegistry): Unit {
            val mySqlContainer = MySqlContainer.mySqlContainer
            registry.add("spring.datasource.url", mySqlContainer::getJdbcUrl)
            registry.add("spring.datasource.username", mySqlContainer::getUsername)
            registry.add("spring.datasource.password", mySqlContainer::getPassword)
        }
    }

注意点

複数テストクラスでコンテナが共有されるので、クリーンな状態でテストが実行できなくなります。 Before系の処理などで、コンテナの中身をクリーンな状態にしてからテストを行う必要があります。

IntelliJ IDEAを使ってDbSetupのKotlin DSL用コードを生成する

IntelliJ IDEAのDatabase Tools and SQLプラグインの検索結果テーブルからGitHub - Ninja-Squad/DbSetup: An API for populating a database in unit testsのKotlin DSL用のデータセットアップコードを生成してみました。

既存のデータベースからデータセットアップ用のコードを出力するスクリプトIntelliJに登録する

SQL-dbsetup-Multirow.kotlin.groovy · GitHubをダウンロードして、ProjectウィンドウのScratches and Consolesの中のExtensions->Database Tools and SQL->extractorsの中に保存します。 下の画像のようになっていればOKです。

f:id:sioiri:20210129083710p:plain

セットアップコードを生成してみる

Extractorsを変更する

下の画像のようにテーブルの検索結果の右上から先程登録したExtractorsを選択します。 f:id:sioiri:20210129083950p:plain

検索結果から適当な行を選択しコピーする

適当な行を選択してコピーを行うと、選択したExtractorsによってKotlin DSL用のデータセットアップコードが生成されクリップボードに格納されます。

例えば、testテーブルの2レコードを選択してコピーしてみます。

f:id:sioiri:20210129084330p:plain

生成されたコードは以下のようになります。あとは、好きな場所に貼り付ければさくっと利用可能になります。

insertInto("test") {
columns("id", "name")
values(1, "c")
values(2, "hoge")
}

おわり。

Spring Boot2.4.0からのapplication.properties(yml)の変更点φ(..)メモメモ

Spring Boot Config Data Migration Guide · spring-projects/spring-boot Wiki · GitHubにまとまっていますが、2.4.0からConfigファイル周りの設定値や読み込み順などが変わっているようです。

2.4.0より前のバージョン

こんな感じにspring.profilesでプロファイル名を指定して、そのプロファイルがアクティブなときの設定値を書いていきます。

app:
  key1:default
---
spring:
  profiles: prod
app:
  key1: prod
---
spring:
  profiles: dev
app:
  key1: dev

複数プロファイルがアクティブな場合の適用順

アクティブなプロファイルが1つだけの場合は、問題ありませんが複数指定した場合は後勝ちで適用されます。

prod,devと指定した場合

2020-11-17 09:13:18.126  INFO 33098 --- [           main] siosio.boot240.Boot240ApplicationKt      : The following profiles are active: prod,dev
2020-11-17 09:13:18.606  INFO 33098 --- [           main] siosio.boot240.Boot240ApplicationKt      : Started Boot240ApplicationKt in 0.736 seconds (JVM running for 1.231)
props = Props(key1=dev)

dev,prodと指定した場合

2020-11-17 09:21:17.252  INFO 34864 --- [           main] siosio.boot240.Boot240ApplicationKt      : The following profiles are active: dev,prod
2020-11-17 09:21:17.856  INFO 34864 --- [           main] siosio.boot240.Boot240ApplicationKt      : Started Boot240ApplicationKt in 0.845 seconds (JVM running for 1.31)
props = Props(key1=prod)

2.4.0以降のバージョン

spring.config.activate.on-profileでプロファイル名を指定して、そのプロファイルがアクティブなときの設定値を書いていきます。 spring.profilesは非推奨になっているので、IntelliJ IDEAの場合そのキー非推奨だよと親切に教えてくれます。

---
spring:
  config:
    activate:
      on-profile: prod
app:
  key1: prod
---
spring:
  config:
    activate:
      on-profile: dev
app:
  key1: dev

複数プロファイルがアクティブな場合の適用順

複数指定した場合でも、設定値の記述順がより下のものが優先されます。上の設定ファイルの場合、常にdevが優先されることになります。

prod,devと指定した場合

2020-11-17 09:31:10.295  INFO 36947 --- [           main] siosio.boot240.Boot240ApplicationKt      : The following profiles are active: prod,dev
2020-11-17 09:31:10.781  INFO 36947 --- [           main] siosio.boot240.Boot240ApplicationKt      : Started Boot240ApplicationKt in 0.741 seconds (JVM running for 1.158)
props = Props(key1=dev)

dev,prodと指定した場合

2020-11-17 09:31:39.106  INFO 37082 --- [           main] siosio.boot240.Boot240ApplicationKt      : The following profiles are active: dev,prod
2020-11-17 09:31:39.557  INFO 37082 --- [           main] siosio.boot240.Boot240ApplicationKt      : Started Boot240ApplicationKt in 0.687 seconds (JVM running for 1.157)
props = Props(key1=dev)

2.4.0より前の動きで動作させたい場合

spring.config.use-legacy-processing=true設定値や起動時のシステムプロパティなどで指定することで、古いバージョンと全く同じ動きとなります。

IntelliJ IDEA2020.3のEAPにしたらLombok使ってるプロジェクトのビルドができなくなったお話

IntelliJ IDEAのバージョン

2020.3 EAPの以下のビルドバージョン(これより前のビルドからこの問題が発生しています) f:id:sioiri:20201016061218p:plain

再現コード

pom.xml

  <dependencies>
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <version>1.18.12</version>
      <scope>provided</scope>
    </dependency>
  </dependencies>

コード

public class Main {

    public static void main(String[] args) {
        final Name name = new Name("name");
        System.out.println("name.value = " + name.getValue());
    }
    
    @Value
    static class Name {
        private final String value;
    }

}

ビルド結果

ビルド通るはずのコードなのに、IntelliJ IDEAでビルドするとでエラーになってしまいますね…

lombok-test/src/main/java/Main.java:6:27
java: クラス Main.Nameのコンストラクタ Nameは指定された型に適用できません。
  期待値: 引数がありません
  検出値: java.lang.String
  理由: 実引数リストと仮引数リストの長さが異なります
lombok-test/src/main/java/Main.java:7:50
java: シンボルを見つけられません
  シンボル:   メソッド getValue()
  場所: タイプMain.Nameの変数 name

ビルド時のログには、Lombokのワーニングが出力されています。

java: You aren't using a compiler supported by lombok, so lombok will not work and has been disabled.
  Your processor is: com.sun.proxy.$Proxy30
  Lombok supports: OpenJDK javac, ECJ

対応方法

[BUG] Lombok Does not work with IntelliJ EAP 2020.3 Build 203.4203.26 · Issue #2592 · rzwitserloot/lombok · GitHubにあるるように、 -Djps.track.ap.dependencies=false をオプションで指定するか、Lombokのバージョンを1.18.14に上げるかですね。

Lombokのバージョンを上げられない場合には、↓の手順でオプションを指定してあげます。 f:id:sioiri:20201016065800p:plain

オプションを実行後ビルドを実行すると、↓なワーニングがでますがLombokを使っているコードのビルドは通るようになります。

java: JPS incremental annotation processing is disabled. Compilation results on partial recompilation may be inaccurate. Use build process "jps.track.ap.dependencies" VM flag to enable/disable incremental annotation processing environment.

Spring Cloud Contractを試してみた

Spring Cloud Contractとは

Spring Cloud Contractは、Consumer-Driven Contract testing1を実現するためのフレームワークです。

お試し構成

Producerをserver-serviceとして、Consumerをclient-serviceとして構築しています。

f:id:sioiri:20200831091436p:plain

Producer(server-service)

build.gradle

dependenciesにspring-cloud-starter-contract-verifierをpluginにorg.springframework.cloud.contractを追加します。これで、契約を元にしたテストコードの生成と実行、Stubの生成などが行われるようになります。

設定を変更したい場合には、contractsに対してオプションを指定します。指定できるオプションは、Spring Cloud Contract: Configuration Optionsにまとまっています。 baseClassForTestsについては、生成されるテストコードの親クラスを指定します。

import org.springframework.cloud.contract.verifier.config.TestFramework

plugins {
  id "org.springframework.cloud.contract" version scContractVersion
}
apply plugin: 'maven'

dependencyManagement {
  imports {
    mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
  }
}

dependencies {
  testImplementation("org.springframework.cloud:spring-cloud-starter-contract-verifier")
}

contracts {
  testFramework = TestFramework.JUNIT5
  basePackageForTests = 'siosio'
  baseClassForTests = 'siosio.BaseTestClass'
}

アプリケーション側の実装

GET sample/{id}で受け取ったidnameを追加したjsonを返すだけのサーバサイドにしています。

package siosio

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.http.HttpStatus
import org.springframework.stereotype.Service
import org.springframework.web.bind.annotation.*

@SpringBootApplication
class ServerSideApplication

fun main(args: Array<String>) {
    runApplication<ServerSideApplication>(*args)
}

@RestController
@RequestMapping("/sample")
class SampleController(
    private val sampleService: SampleService
) {

    @GetMapping("{id:\\d+}")
    fun get(@PathVariable id: Int): SampleResponse {
        return sampleService.get(id)
    }
}

@Service
class SampleService {
    fun get(id: Int): SampleResponse {
        return SampleResponse(id, "name")
    }
}

data class SampleResponse(
    val id: Int,
    val name: String,
)

@ResponseStatus(HttpStatus.NOT_FOUND)
class NotFoundException : RuntimeException()

テストコードの親クラス

build.gradleで指定した親クラスの実装になります。 検証対象のエンドポイントを持つControllerがどれかを、RestAssuredMockMvc.standaloneSetupに設定します。 Controllerが依存しているServiceをモック化したい場合には、このクラスでモックを生成して設定してあげることができます。

abstract class BaseTestClass {

    private val mockService: SampleService = mockk()

    @BeforeEach
    open fun setup() {
        val slot = slot<Int>()
        every { mockService.get(capture(slot)) } answers {
            if (slot.captured == 999) throw NotFoundException()
            SampleResponse(slot.captured, "name_${slot.captured}")
        }
        RestAssuredMockMvc.standaloneSetup(SampleController(mockService))
    }
}

Contractの定義

Contractはデフォルト設定では、src/t/est/resources/contracts に定義します。今回は、正常系と404を返すケースのContractを定義してみます。 Contractの定義方法は、Contract DSLを見るとだいたいわかります。

正常系(src/t/est/resources/contracts/GetSample.groovy)

SampleControllerで定義しているエンドポイントを呼び出すrequestを定義しています。 responseでは、requestで送ったidnameが戻ってくることを定義します。

import org.springframework.cloud.contract.spec.Contract

Contract.make {
  priority 2

  request {
    method('GET')
    url "/sample/${regex('\\d+')}"
  }

  response {
    status(OK())

    headers {
      contentType(applicationJson())
    }
    
    body(
        id: fromRequest().path(1),
        name: "name_${fromRequest().path(1)}"
    )
  }
}

404系(src/t/est/resources/contractsGetSample_404.groovy)

期待するresponseとして404を定義します。priorityを正常系より高くして404を優先的に扱えるようにしてあげます。

import org.springframework.cloud.contract.spec.Contract

Contract.make {
  priority 1
  request {
    method('GET')
    url ("/sample/999")
  }

  response {
    status(NOT_FOUND())
  }
}

Stubの生成とMavenローカルリポジトリへのインストール

buildとインストールタスクを実行してStubの生成とMavenローカルリポジトリへのインストールを行います。 なお、Producer側のContractに従っているかの検証だけであれば、./gradlew testだけでOKです。

./gradlew  build install

テストコードが生成され、Contractに従っているかの検証が行われます。 今回は、正常系と404系の2ケースともContractに従っていることがわかります。

f:id:sioiri:20200901061109p:plain

Consumer(client-service)

build.gradle

dependenciesにspring-cloud-starter-contract-stub-runnerを追加します。これで、Producer側で生成したStubを使用したテストコードがかけるようになります。

dependencies {
  testImplementation("org.springframework.cloud:spring-cloud-starter-contract-stub-runner")
}

Producer側を呼び出すコード

RestTemplateを使ってエンドポイントを叩くだけの実装です。実際は、urlは設定ファイルに切り出したりとかが必要になると思いますが…

@Service
class SampleService(
    private val restTemplate: RestTemplate
) {
    fun get(id: Int): SampleApiResponse {
        return restTemplate.getForEntity("http://localhost:8080/sample/{id}", SampleApiResponse::class.java, id).body!!
    }
}

data class SampleApiResponse(
    val id: Int,
    val name: String,
)

テストコード

Producer側と違い、テストコードを作成する必要があります。

AutoConfigureStubRunnerアノテーションを使ってどのStubを使うのかを指定します。 idアノテーションにstubの情報を指定します。大事なのは、最後に指定するポート(8080の部分)は実際にProducerを呼び出す際に指定しているポートを指定します。 Mavenのローカルリポジトリを使っているので、stubsModeにはLOCALを指定します。 リモートリポジトリを使う場合には、REMOTEを設定しrepositoryRootを設定する必要があるようです。

package siosio

import org.assertj.core.api.Assertions
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.cloud.contract.stubrunner.spring.AutoConfigureStubRunner
import org.springframework.cloud.contract.stubrunner.spring.StubRunnerProperties
import org.springframework.test.context.TestConstructor
import org.springframework.test.context.junit.jupiter.SpringExtension
import org.springframework.web.client.HttpClientErrorException

@TestConstructor(autowireMode = TestConstructor.AutowireMode.ALL)
@ExtendWith(SpringExtension::class)
@SpringBootTest
@AutoConfigureStubRunner(ids = ["siosio:server-service:1.0.0:stubs:8080"], stubsMode = StubRunnerProperties.StubsMode.LOCAL)
internal class SampleServiceTest(
    private val sut: SampleService
) {

    @Test
    internal fun 正常系() {
        val actual = sut.get(100)
        Assertions.assertThat(actual)
            .isEqualTo(SampleApiResponse(100, "name_100"))
    }
    
    @Test
    internal fun `404系`() {
        Assertions.assertThatThrownBy { sut.get(999) }
            .isInstanceOf(HttpClientErrorException.NotFound::class.java)
    }
}

テスト結果

テスト成功ですね。これでConsumer側もContractに従っていることが検証できました。

f:id:sioiri:20200901062957p:plain

ソースコード全体

github.com