しおしお

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

JUnit Pioneerを使ってJUnit5のテスト実行時に環境変数を設定する

JUnit5なテスト実行時に環境変数を設定(クリア)できる、拡張のGitHub - junit-pioneer/junit-pioneer: JUnit 5 Extension Packを試してみました。

テスト実行時に環境変数を設定する

環境変数の設定は@SetEnvironmentVariableアノテーションで行います。 テストクラス、テストメソッド両方に指定ができるので、どのようなスコープで設定するかで使い分けることができるようになっています。

@SetEnvironmentVariable(key = "k1", value = "v1")
class HogeTest {

    @Test
    @SetEnvironmentVariable(key = "k2", value = "v2")
    fun 環境変数を設定してみる() {
        println("System.getenv(\"k1\") = ${System.getenv("k1")}")
        println("System.getenv(\"k2\") = ${System.getenv("k2")}")
    }
}

実行結果

設定した環境変数が設定され、参照できているのが確認できます。

System.getenv("k1") = v1
System.getenv("k2") = v2

テスト実行後に環境変数をクリアする

環境変数のクリアは@ClearEnvironmentVariableアノテーションで行います。 @SetEnvironmentVariableと同じように、テストクラスとテストメソッドの両方に指定が可能となっています。

    @Test
    @SetEnvironmentVariable(key = "k2", value = "v2")
    @ClearEnvironmentVariable(key = "k2")
    fun 環境変数を設定してみる() {
        println("System.getenv(\"k1\") = ${System.getenv("k1")}")
        println("System.getenv(\"k2\") = ${System.getenv("k2")}")
    }

おわり

Vue.js3でのv-modelのメモ

Vue.jsのバージョン3では、v-modelの仕様が結構変わっているのでそれのメモ。

サンプルコード

シンプルなパターン

modelValueプロパティを使った値の受け渡しになっていて、値の変更は、update:modelValueイベントを発火することで親コンポーネントに通知できるようになっています。
※バージョン2の頃の、valueプロパティとinputイベントの発火からは変更になっています。

子供側コンポーネントのコード

export default {
  name: 'ChildComponent',
  props: {
    modelValue: String,
  },
  data: () => {
    return {
      i: 1
    }
  },
  methods: {
    change: function () {
      const count = this.i++;
      this.$emit('update:modelValue', `${this.modelValue.replace(/:.+$/, '')}: ${count}`)
    }
  }
}

親側のコンポーネント

<template>
  <child v-model="str" />
</template>

名前付きのv-modelを使うパターン

v-modelの引数に名前を指定することで、その名前のプロパティを使った受け渡しが可能になります。 子供のコンポーネントでは、任意の名前でプロパティを定義して、値の変更はupdate:プロパティ名で発火することで親コンポーネントに通知できるようになっています。

子供側のコンポーネント

export default {
  name: 'ChildComponent',
  props: {
    name: String
  },
  methods: {
    change() {
      this.$emit('update:name', '変更されたよ')
    }
  }
}

親側のコンポーネント

<template>
  <child v-model:name="str" />
</template>

KotestのSpring拡張を試してみる

KotestSpring拡張を使う方法になります。

KotestのSpring拡張をdependenciesに追加

io.kotest:kotest-runner-junit5-jvmだけではなく、Spring拡張のio.kotest.extensions:kotest-extensions-springを追加します。

Mockkを使うのに便利なcom.ninja-squad:springmockkもセットで追加しておきます。

testImplementation("io.kotest:kotest-runner-junit5-jvm:5.1.0")
testImplementation("io.kotest.extensions:kotest-extensions-spring:1.1.0")

testImplementation("com.ninja-squad:springmockk:3.1.0")

テストを書いてみる

コンポーネントのテスト

テスト対象のコード

テスト対象は、Repositoryを呼び出して結果を詰め替えして返すだけのシンプルな実装としています。

@Transactional(readOnly = true)
class FindUsersUseCase(private val userRepository: UserRepository) {
    fun findAllUser(): List<UserDto> {
        return userRepository.findAll()
                .map { UserDto(it.id, it.name) }
    }
}

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

テストコード

JUnit と同じように@SpringBootTestアノテーションを使ってテスト対象のコンポーネントを指定します。 KotestのSpring拡張はこのアノテーションのみでコンストラクターインジェクションを使ってテストが実行できるようです。

テスト対象の中で依存しているRepositoryをモックに差し替えてテストしたいので、dependenciesに追加したcom.ninja-squad:springmockk@MockkBeanアノテーションを使用してモックを生成しています。

SpringExtensionextensionsに登録することで、Kotestのライフサイクルの中でTestExecutionListenerが動作するようになります。(例えばこの機能で、Mockkのモックのクリア処理がテスト実行後に自動的に行われるようになったりします。)

@SpringBootTest(classes = [FindUsersUseCase::class])
@ActiveProfiles("test")
class FindUsersUseCaseTest(
    private val sut: FindUsersUseCase,
    @MockkBean val mockUserRepository: UserRepository
) : FreeSpec({

    extensions(SpringExtension)

    "ユーザ一覧が取得できるよ" {
        every { mockUserRepository.findAll() } returns listOf(User(1, "name_1"), User(2, "name_2"), User(3, "name_3"))

        sut.findAllUser() shouldBe listOf(UserDto(1, "name_1"), UserDto(2, "name_2"), UserDto(3, "name_3"))

        verify { mockUserRepository.findAll() }
    }
})

SpringExtensionをすべてのテストクラスで登録するのはかなり面倒かつ漏れたりすると思うので、下のようなプロジェクト全体設定で登録しておくといい感じになります。

class ProjectConfig : AbstractProjectConfig() {
    override fun extensions() = listOf(SpringExtension)
}

テストの実行結果テストも期待通りに実行できています。

f:id:sioiri:20220211214250p:plain

MockMvcを使ったテスト

テストコード

基本的なテストの書き方は、コンポーネントのテストと同じになります。 MockMvcを利用するために、JUnitと同じように@AutoConfigureMockMvcアノテーションを設定します。

@SpringBootTest
@AutoConfigureMockMvc
internal class UserHandlerTest(
    val mockMvc: MockMvc,
    val dataSource: DataSource,
    val flyway: Flyway
) : FreeSpec({

    extension(SpringExtension)

    beforeTest {
        flyway.clean()
        flyway.migrate()
    }

    "ユーザが登録できるよ" {
        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")
    }

    "ユーザ一覧が取得できるよ" {
        mockMvc.get("/api/users")
            .andExpect {
                status { isOk() }
                content { contentType(MediaType.APPLICATION_JSON) }
                jsonPath("$.users.length()") { value(3) }
                jsonPath("$.users[*].name") { value(contains("user_1", "user_2", "user_3")) }
                jsonPath("$.users[*].id") { value(contains(1, 2, 3)) }
            }
    }
}) {
    companion object {
        @Suppress("unused")
        val container = MySQLContainer<Nothing>("mysql:8.0.28").run {
            start()
            waitingFor(HostPortWaitStrategy())

            System.setProperty("spring.datasource.url", jdbcUrl)
            System.setProperty("spring.datasource.username", username)
            System.setProperty("spring.datasource.password", password)
        }
    }
}

おわり。

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"}""")
    }
}