しおしお

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

KotestでTestcontainersを使ってみる

KotestTestcontainersを簡単に使う方法になります。

KotestのTestcontainers拡張を追加

KotestにはTestcontainers用の拡張ライブラリが用意されているので、それを依存に追加します。

build.gradleの場合

testImplementation 'io.kotest.extensions:kotest-extensions-testcontainers:1.1.1'

pom.xmlの場合

<dependency>
    <groupId>io.kotest.extensions</groupId>
    <artifactId>kotest-extensions-testcontainers</artifactId>
    <version>1.1.1</version>
    <scope>test</scope>
</dependency>

デフォルト構成Testcontainerを使ってみる

テストコード

WireMockのコンテナをTestcontainersで起動するテストコードになります。 TestContainerExtensionにimageを指定して、installすることでコンテナを上げることができます。

class TestContainerExample : FreeSpec({
    val container = install(TestContainerExtension("wiremock/wiremock:latest")) {
        withExposedPorts(8080)
    }

    (1..5).forEach {
        "test $it" {
            println("test $it")
            println("container.containerId = ${container.containerId}")
            println("container.getMappedPort(8080) = ${container.getMappedPort(8080)}")
        }
    }

})

実行結果

実行結果を見ると、コンテナが起動され公開されたポートがホスト側のポートにマッピングされていることが確認できます。 デフォルトでは、全てのテストケースで同じコンテナを使いまわしするようです。(Spec単位でコンテナが管理されます)

test 1
container.containerId = 74716ee4e1bf41bb73012c95648698f2a22b06e1ff670dbc187096735fa39bb2
container.getMappedPort(8080) = 49190
test 2
container.containerId = 74716ee4e1bf41bb73012c95648698f2a22b06e1ff670dbc187096735fa39bb2
container.getMappedPort(8080) = 49190
test 3
container.containerId = 74716ee4e1bf41bb73012c95648698f2a22b06e1ff670dbc187096735fa39bb2
container.getMappedPort(8080) = 49190
test 4
container.containerId = 74716ee4e1bf41bb73012c95648698f2a22b06e1ff670dbc187096735fa39bb2
container.getMappedPort(8080) = 49190
test 5
container.containerId = 74716ee4e1bf41bb73012c95648698f2a22b06e1ff670dbc187096735fa39bb2
container.getMappedPort(8080) = 49190

テストケース単位でコンテナを使い捨てる構成にしてみる

テストコード

TestContainerExtensionLifecycleMode.EveryTestを指定することで、テストケース単位にコンテナが起動されクリーンな状態でテストが実行できるようになります。

class TestContainerExample : FreeSpec({
    val container = install(TestContainerExtension("wiremock/wiremock:latest", LifecycleMode.EveryTest)) {
        withExposedPorts(8080)
    }

    (1..5).forEach {
        "test $it" {
            println("test $it")
            println("container.containerId = ${container.containerId}")
            println("container.getMappedPort(8080) = ${container.getMappedPort(8080)}")
        }
    }

})

実行結果

実行結果を見るとテストごとにコンテナIDが変わっていることが確認できるので、LifecycleModeの指定が効いていることが確認できます。

test 1
container.containerId = 74bd2ffd8304eebbdb57281b5e7bd818f379036caf314fe36714ba877edd80f3
container.getMappedPort(8080) = 49192
test 2
container.containerId = a278dd9a16a066e1dd0beb5785c4b61ec3088c96c89d9bbc8deedb0f2cd7d99c
container.getMappedPort(8080) = 49193
test 3
container.containerId = 6b47542b908d6da7b2a5a9aad7997840b0c626fd08851c1f0140691629e9f861
container.getMappedPort(8080) = 49194
test 4
container.containerId = 54dedc990cde75c608c0491f1d12d4cc7ee779a0e24587665341929f3a5b3108
container.getMappedPort(8080) = 49195
test 5
container.containerId = f4237335e4d9bb167dc07b1b466f23b2e22101c1d0dbc3470e8092688c034e7d
container.getMappedPort(8080) = 49196

データベースを扱ってみる

データベースを扱う際には、Testcontainersの使用したいデータベースのモジュールを使用してコンテナを上げるのが簡単です。 テストコードで、データベースにアクセスしたい場合には、 JdbcTestContainerExtension を使用することで立ち上げたコンテナに対する接続を持ったHikariDataSourceを取得することができます。 HikariDataSourceがあれば、データのセットアップ系などの処理も簡単に行えるので良さそうですね。

build.gradle

  testImplementation "org.testcontainers:postgresql:1.16.3"
  testRuntimeOnly 'org.postgresql:postgresql:42.3.1'

テストコード

class TestContainerExample : FreeSpec({
    val postgresql = PostgreSQLContainer<Nothing>("postgres:9.6.24-stretch")
    val datasource = install(JdbcTestContainerExtension(postgresql)) {
        poolName = "test-pool"
    }

    "test" {
        datasource.connection.metaData.databaseProductVersion shouldBe "9.6.24"
    }
})

おわり。

IntelliJ IDEAのCloud Codeプラグインを使ってSkaffoldしてみたよ

IntelliJ IDEAのCloud CodeプラグインのSkaffold機能を試してみたいと思います。

準備

Cloud Code - IntelliJ IDEs Plugin | Marketplaceをお使いのIntelliJ IDEAにインストールします。 Supported Productsに、Communityの記載があるのでUltimate以外でも使えそうです。

f:id:sioiri:20211222161048p:plain:w600

Skaffold.ymlの準備

Spring Bootなアプリケーションを起動するためのSkaffold.ymlを準備します。
k8sマニフェストを作っている場合、skaffold initで生成することもできます。

apiVersion: skaffold/v2beta26
kind: Config
metadata:
  name: skaffold-example
build:
  artifacts:
  - image: siosio/hello
    buildpacks:
      builder: gcr.io/buildpacks/builder:v1
  local:
    push: false
    useBuildkit: true
deploy:
  kubectl:
    manifests:
    - k8s/deployment.yaml
    - k8s/service.yaml

実行構成を作成する

  1. 下の画像のように実行構成のCloud Code: Kubernetesを選択し、新規の実行構成を作成します。
  2. RunタブのDeploymentからデプロイ対象を選択します。
  3. RunタブのWatch modeからファイル変更後のリビルド方法を選択します。
    ファイルの保存のたびにリビルドが実行されるのが鬱陶しい場合には、デフォルトのOn demandで良いかと思います。
  4. Build/DeployタブのSkaffold configurationのプルダウンから対象のskaffold.ymlを選択します。
  5. 最後に保存して終了です。

f:id:sioiri:20211222161443p:plain:w600

※現状の機能では、細かなオプションの指定などはできなさそうな感じでした。

実行(デプロイ)する

作成した実行構成から起動(デプロイ)します。

f:id:sioiri:20211222162552p:plain:w600

起動の確認

起動が成功すると、Runウィンドウからポートフォワードされローカルからアクセス可能なサービスの一覧が見れたりするようです。

f:id:sioiri:20211222162844p:plain:w600

Cloud CodeプラグインKubernetes Explorerもデプロイが成功したことが確認できます。

f:id:sioiri:20211222163120p:plain:w600

Kubernetes - IntelliJ IDEs Plugin | Marketplaceでも同じ情報を見れますが、Cloud CodeプラグインはCommunity版でも使えるメリットがあるかなと思います。

ファイルを変更して再デプロイ

実行構成で、Run -> Watch modeをデフォルトのOn demandにしている場合には、RunウィンドウのコンソールでCtrl + Alt + ,(カンマ)を入力することで再デプロイを行えます。 ファイル変更後に自分のタイミングでコンソールからデプロイの指示を行えば良いので、無駄なデプロイが走らず良い感じです。

デバッグ実行してみる

Skaffoldでデプロイしたアプリケーションのデバッグをしたい場合には、IntelliJ IDEAからデバッグモードで実行(デプロイ)します。

f:id:sioiri:20211222163848p:plain:w600

デバッグモードで実行すると、デプロイ完了後に自動的にリモートデバッグ用のクライアント側が実行されるようです。 Skaffoldで起動したSpring BootなアプリケーションをIntelliJ IDEAでデバッグする - しおしおのように、自分で実行構成準備しなくてよいのがいいですね。

f:id:sioiri:20211222164258p:plain:w600

ファイル変更後にOn demandで再デプロイすることで、リモートデバッグ用のクライアントが自動で再起動もされるようです。

最後に…

Cloud Codeプラグインを入れることで、IntelliJ IDEA上ですべてが行える(ターミナルも不要)のがよいですね!

おわり。

Skaffoldで起動したSpring BootなアプリケーションをIntelliJ IDEAでデバッグする

Skaffoldで起動したSpring BootをIntelliJ IDEAでデバッグする方法を調べてみました。1

デバッグモードでアプリケーションをデプロイする

skaffold debugコマンドを使用して、アプリケーションをデプロイします。 debugコマンドを使ってデプロイすることで、ドキュメントに記載があるようにリモートでのデバッグが可能となります。 Javaのアプリケーションについては、ドキュメントに記載があるようにJDWPエージェントが有効化されるようです。

SkaffoldのConfigファイルをデフォルトのSkaffold.ymlで作っている場合には、以下のコマンドを実行するだけとなります。

skaffold debug --port-forward

IntelliJ IDEAへデバッグ設定を追加

下の画像のように、実行構成からRemote JVM Debugを追加します。 デフォルトの設定で、skaffold debugコマンドで有効化されたJDWPエージェントに接続されるので実行構成は特に変更しなくても問題ありません。 f:id:sioiri:20211219181610p:plain:w800

リモートデバッグをしてみる

下の動画のようにSkaffoldでデプロイしたアプリケーションに対してIntelliJ IDEAからリモートデバッグが可能となります。 f:id:sioiri:20211219182208g:plain

注意点

skaffold devコマンドと異なり、ファイルの変更監視や変更を検知しての自動デプロイはデフォルトでは無効化されています。 devコマンドと同じように、デバッグ中に変更検知と自動デプロイを使いたい場合には、下のようにauto系のオプションを追加してdebugコマンドを実行ます。

ただし、自動デプロイが行われるリモートデバッグの接続が切れるため、再度デバッグしたい場合にはRemote JVM Debug`を再実行する必要があります。

skaffold debug --port-forward --auto-build --auto-deploy --auto-sync

おわり。


  1. Spring Boot以外でもJavaなアプリケーションであれば方法でデバッグできます。

WireMock clientのKotlin DSLを作ってみた

WireMock clientのKotlin DSLを作ってみたお話です。

導入

Maven

<dependency>
  <groupId>io.github.siosio</groupId>
  <artifactId>wiremockk</artifactId>
  <version>1.0.0</version>
</dependency>

Gradle

implementation 'io.github.siosio:wiremockk:1.0.0'

使い方

Stubの登録

WireMockに追加された拡張関数のregisterを使ってStubを登録できるようになっています。 registerには、requestに指定したパターンにマッチした場合に返すものをresponseに指定して登録します。

基本の形

この例の場合、以下が完全にマッチした場合に、指定のレスポンスが返ります。

  • method
  • url
import io.github.siosio.wiremockk.register

val wireMock = WireMock(container.getMappedPort(8080))

wireMock.register {
    request {
        method = RequestMethod.GET
        url = "/test"
    }
    response {
        status = 200
        headers {
            contentType("application/json")
        }
        body {
            // classpath配下のファイルの内容がレスポンスとして返されます。
            path("data/test.json")
        }
    }
}

クエリパラメータのパターン登録

クエリーパラメータのパターンは、requestブロックのurlブロックに指定します。 この例の場合には、クエリーパラメータのkeyabがありqにはvが含まれている場合に、指定のレスポンスが返されるようになります。

request {
    method = RequestMethod.GET
    url("/test") {
        // valueに文字列を指定した場合は、WireMock.equalToと同じ意味になります
        queryParam("key", "a")
        queryParam("key", "b")
        queryParam("q", WireMock.containing("v"))
    }
}

リクエストボティのパターン登録

bodyjsonxmlを使って、リクエストボディがマッチしたときにレスポンスを返すStubを定義できます。 jsonPathを使うことでclasspath配下のファイルの内容とマッチさせることもできます。 任意のパターンを使ってマッチさせたい場合には、patternを使います。

import io.github.siosio.wiremockk.register

val wireMock = WireMock(container.getMappedPort(8080))

wireMock.register {
    request {
        method = RequestMethod.POST
        url = "/users"
        body {
            // language=json
            json(
                """
                {
                  "user": {
                    "name": "siosio"
                  }
                }
            """.trimIndent(), false, true)
        }
    }
    response {
        status = 201
        headers {
            header("location", "users/1")
        }
    }
}

リクエストの検証

WireMockに追加された拡張関数のverifyを使って検証ができるようになっています。

検証例

基本的な使い方は、registerrequestブロックの指定と同じになっています。

wireMock.verify {
    url = "/test"
    method = RequestMethod.POST
    headers {
        header("Content-Type", "application/json")
    }
    body {
        json("""{"test":"value"}""")
    }
}

WebStormでDenoを使ってみる

お仕事でDenoを使う機会があったので、WebStormで環境構築や実行&デバッグできるか試してみました。

使用したバージョン

Deno

$ deno --version
deno 1.14.1 (release, x86_64-unknown-linux-gnu)
v8 9.4.146.15
typescript 4.4.2

WebStorm

WebStorm 2021.3 EAP

WebStormにDeno環境を準備

プラグインのインストール

WebStorm用のDenoプラグインがあるので、インストールしてあげます。

プラグインの有効化

設定画面の、Languages & Frameworks > Denoからプロジェクトに対するDenoサポートを有効にしてあげます。 f:id:sioiri:20210924132701p:plain

実行

コードの準備

ファイルを読み込んで標準出力に出力するだけの簡単なコードを準備してみました。

const data = await Deno.readTextFile('data.json')
console.log(data)

実行

実行は、メニューのRun > Run..から行います。Denoプラグインを有効化していると自動的にDenoでの実行が選択されるようです。

実行時に権限の追加を行う場合には、実行構成の画面からArgumentsを編集してあげます。 今回は、ファイルを読み込むので--allow-readを追加しています。 f:id:sioiri:20210924134426p:plain

デバッグ実行

デバッグ実行は、ブレークポイントを設定してデバッグ実行してあげるだけになります。 あとは、ブレークポイントを設定した場所で止まってくれるので、状態を見に行ったりステップ実行などをしてあげる感じになります。 f:id:sioiri:20210924134750p:plain

Spring Data R2DBCで複数のデータベースに接続してみる

やりたいこと

単一のWebFluxのアプリケーションで、Spring Data R2DBCを使って複数のデータベースに接続してSQLを実行したい。

環境準備

データベース

準備が簡単なので、Docker Composeを使ってデータベースを2つ起動しています。

version: '3.7'
services:
  db1:
    image: 'postgres'
    ports:
      - "15432:5432"
    environment:
      - POSTGRES_HOST_AUTH_METHOD=trust
  db2:
    image: 'postgres'
    ports:
      - "15433:5432"
    environment:
      - POSTGRES_HOST_AUTH_METHOD=trust

テーブル

アプリケーションで使用するテーブルをそれぞれのデータベースに作成してあげます。

db1

create table hoge (
    id   char(36) not null,
    name varchar(100),
    primary key (id)
);

db2

create table fuga (
    id   serial not null,
    name varchar(100),
    primary key (id)
);

アプリケーションの実装

Spring Bootバージョン

Spring Bootは現時点で最新のバージョンを使って試してみます。

  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.5.4</version>
    <relativePath /> <!-- lookup parent from repository -->
  </parent>

データベースの接続を切り替えるための実装

Configurationの準備

db1とdb2に接続するためのConfigurationを準備します。

Configurationには@EnableR2dbcRepositoriesアノテーションを追加し、この接続設定を使うベースパッケージをbasePackagesに指定します。 また、指定したベースパッケージ配下で利用するR2dbcEntityTemplateのBean名をentityOperationsRefに指定します。 これらを設定してあげるだけで、パッケージ単位に利用するR2dbcEntityTemplateが自動的に切り替わるので、複数のデータベースへのアクセスが可能になります。

@Configuration
@EnableR2dbcRepositories(
    basePackages = ["siosio.multipler2dbc.db1"],
    entityOperationsRef = "db1EntityTemplate"
)
class Db1Config {

    @Bean
    @ConfigurationProperties(prefix = "db1")
    fun db1Properties(): R2dbcProperties = R2dbcProperties()

    @Bean
    fun db1ConnectionFactory(): ConnectionFactory = db1Properties().toConnectionFactory()

    @Bean
    fun db1EntityTemplate() = R2dbcEntityTemplate(db1ConnectionFactory())
}

@Configuration
@EnableR2dbcRepositories(
    basePackages = ["siosio.multipler2dbc.db2"],
    entityOperationsRef = "db2EntityTemplate"
)
class Db2Config {

    @Bean
    @ConfigurationProperties(prefix = "db2")
    fun db2Properties(): R2dbcProperties = R2dbcProperties()

    @Bean
    fun db2ConnectionFactory(): ConnectionFactory = db2Properties().toConnectionFactory()

    @Bean
    fun db2EntityTemplate() = R2dbcEntityTemplate(db2ConnectionFactory())
}

fun R2dbcProperties.toConnectionFactory(): ConnectionFactory {
    val connectionFactory = ConnectionFactoryOptions.parse(url)
        .mutate()
        .apply {
            username?.let { option(ConnectionFactoryOptions.USER, it) }
            password?.let { option(ConnectionFactoryOptions.PASSWORD, it) }

        }.build().let {
            ConnectionFactories.get(it)
        }
    val poolConfiguration = ConnectionPoolConfiguration
        .builder(connectionFactory)
        .maxSize(pool.maxSize)
        .initialSize(pool.initialSize)
        .build()
    return ConnectionPool(poolConfiguration)
}

接続先の設定

db1db2に対する接続先の情報をproperties(yml)に設定します。 今回は、db1db2としてConfigurationクラスにプレフィックスと指定しているので、設定値はdb1.またはdb2.から始まります。 プレフィクス以降は、R2dbcPropertiesに対して設定可能なキーを指定してあげます。

db1.url=r2dbc:postgresql://localhost:15432/postgres
db1.username=postgres

db2.url=r2dbc:postgresql://localhost:15433/postgres
db2.username=postgres

データベースに対するアクセスする側の実装

それぞれのデータベースに対してアクセスするには、Configurationで指定したパッケージ配下にデータベースに対する処理を実装するだけとなります。 今回の例では、次ののように実装するだけで自動的に接続先が切り替わります。

  • db1に対する処理は、siosio.multipler2dbc.db1パッケージ配下に実装します。
  • db2に対する処理は、siosio.multipler2dbc.db2パッケージ配下に実装します。

あとは、単一データベース接続のときと同じようにデータベースアクセスするだけですね。1

db1への処理

package siosio.multipler2dbc.db1

// importは省略

@Component
class Db1Handler(private val hogeRepository: HogeRepository) {
    suspend fun post(request: ServerRequest) : ServerResponse {
        val hoge = hogeRepository.save(Hoge(UUID.randomUUID().toString(), "hoge")).awaitSingle()
        return ServerResponse.ok().bodyValueAndAwait(mapOf("id" to hoge.id))
    }
}

@Repository
interface HogeRepository: ReactiveCrudRepository<Hoge, String>

//create table hoge (id char(36) not null, name varchar(100), primary key(id))
data class Hoge(
    @Id private val id: String,
    val name: String
) : Persistable<String> {
    override fun isNew(): Boolean = true
    override fun getId() = id
}

db2への処理

package siosio.multipler2dbc.db2

// importは省略

@Component
class Db2Handler(private val fugaRepository: FugaRepository) {
    suspend fun post(request: ServerRequest): ServerResponse {
        val fuga = fugaRepository.save(Fuga(name = "fuga")).awaitSingle()
        return ServerResponse.ok().bodyValueAndAwait(mapOf("id" to fuga.id))
    }
}

@Repository
interface FugaRepository : ReactiveCrudRepository<Fuga, Int>

//create table fuga (id serial not null, name varchar(100), primary key(id))
data class Fuga(
    @Id val id: Int? = null,
    val name: String
)

動作確認

curlでそれぞれのエンドポイントを叩くと、エラーなど起きずにデータベースの処理結果のIDが返ってくることが確認できます。 これで、複数データベースへの接続確認はOKですね。

$ curl -X POST http://localhost:8080/db1
{"id":"6c1d2c95-d36f-4b1d-acea-366f6a45e240"}

$ curl -X POST http://localhost:8080/db2
{"id":9}

ソースコード

使用したサンプルプロジェクト全体は、以下から確認できます。 github.com


  1. この実装例では、トランザクション制御は省略しています

Spring Boot + MVCでkotlinx.serializationを使ってリクエスト・レスポンスのシリアライズとデシリアライズをしてみる

Spring Boot + MVCでkotlinx.serializationを使ってリクエスト・レスポンスのシリアライズとデシリアライズを試してみました。

使用バージョンなど

↓のバージョンを使って試してみました。

  • Spring Bootは、2.5.4
  • kotlinx.serialization

build.gradle.ktsはこのようになっています。

import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
    id("org.springframework.boot") version "2.5.4"
    id("io.spring.dependency-management") version "1.0.11.RELEASE"
    kotlin("jvm") version "1.5.30"
    kotlin("plugin.spring") version "1.5.30"
    kotlin("plugin.serialization") version "1.5.30"
}

// 〜省略〜
dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.2")
    implementation("org.jetbrains.kotlin:kotlin-reflect")
    implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
    testImplementation("org.springframework.boot:spring-boot-starter-test")
}

お試ししてみる

コード

実装的には、kotlinx.serializationを使うためにdata classに@Serializableアノテーションが設定されている感じですね。 なお、kotlinx-serialization-jsonに関連するクラスがクラスパス上にあると、自動的にkotlinx.serialization用のHttpMessageConverterが使われるようになります。(優先度的には、Jacksonより高くなる感じです。)

今回は、リクエストボディとレスポンスボディの変換でkotlinx.serializationが使われることを確認できるコードを書いてみました。

@RestController
@RequestMapping("/hello")
class HelloController {

    private val logger = LoggerFactory.getLogger(HelloController::class.java)

    @GetMapping
    fun get(): Message {
        return Message("hello!!!")
    }

    @PostMapping
    fun post(@RequestBody message: Message) {
        logger.info("mesage: {}", message)
    }
}

@Serializable
data class Message(
    val text: String
)

実行結果

レスポンスボディの変換

kotlinx.serializationを使ってるか非常に分かりづらいですが、レスポンスとして指定したdata classの内容がJSONで返ってきていますね。

$ curl  localhost:8080/hello
{"text":"hello!!!"}

リクエストボディの変換

curl -X POST -H 'content-type: application/json' localhost:8080/hello -d '{"text": "siosio!"}'

ログにリクエストボディを保持しているdata classの内容が出力されていますね。

2021-09-05 15:38:02.745  INFO 43536 --- [nio-8080-exec-3] s.b.HelloController                      : mesage: Message(text=siosio!)

カスタマイズして使ってみる

デフォルトの構成では何も設定が入っていない状態で使われるため、data classに定義されていない項目がリクエストのJSONにあったりすると400 Bad Requestが返されたりしちゃいます。 例えば、次のリクエストを投げると、Could not read JSON: Unexpected JSON token at offset 21: Encountered an unknown key 'hoge'. なメッセージのログが出力され400エラーとなります。

curl -X POST -H 'content-type: application/json' localhost:8080/hello -d '{"text": "siosio!", "hoge": "fuga"}'

これでは、ちょっと使い勝手が悪いですよね。これは設定をカスタマイズしたkotlinx.serialization用のHttpMessageConverterを登録するだけで解決します。 カスタマイズしたHttpMessageConverterは、WebMvcConfigurerをimplementsしたクラスを作ってconfigureMessageConvertersをオーバライドすることで登録できます。
↓のコードのように、KotlinSerializationJsonHttpMessageConverterに設定をカスタマイズしたものを指定し、convertersの先頭に追加します。(末尾に追加してしまうと、デフォルト設定のKotlinSerializationJsonHttpMessageConverterが使われちゃうので注意です。)

@Configuration
class WebConfig : WebMvcConfigurer {

    override fun configureMessageConverters(converters: MutableList<HttpMessageConverter<*>>) {
        converters.add(0, KotlinSerializationJsonHttpMessageConverter(Json {
            ignoreUnknownKeys = true
        }))
    }
}