しおしお

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

IntelliJ IDEA2021.1からGitのコミットメッセージテンプレートに対応したのが便利

現時点ではBeta版のIntelliJ IDEAの2021.1からGitのコミットメッセージテンプレートに対応してくれたのが便利ですね。 これを使うことでサードパーティ製のプラグインなど入れなくても、テンプレートを元にメッセージのプレフィックス的なもの入れたりするのも簡単にできますね。

使い方

使い方はgit config commit.templateを使ってテンプレートのメッセージを登録しておくだけですね。 私は、こんな感じに絵文字のリストをコメントとして登録しておいて、コミット時に必要な絵文字をコメントからコピって入力するようにしています。

#{id}

# :tada:        Initial commit
# :bookmark:    Version tag
# :sparkles:    New feature
# :bug:    Bugfix
# :card_index:    Metadata
# :books:    Documentation
# :bulb:    Documenting source code
# :racehorse:    Performance
# :lipstick:    Cosmetic
# :rotating_light:    Tests
# :white_check_mark:    Adding a test
# :heavy_check_mark:    Make a test pass
# :zap:    General update
# :art:    Improve format/structure
# :hammer:    Refactor code
# :fire:    Removing code/files
# :green_heart:    Continuous Integration
# :lock:    Security
# :arrow_up:    Upgrading dependencies
# :arrow_down:    Downgrading dependencies
# :shirt:    Lint
# :alien:    Translation
# :pencil:    Text
# :ambulance:    Critical hotfix
# :rocket:    Deploying stuff
# :apple:    Fixing on MacOS
# :penguin:    Fixing on Linux
# :checkered_flag:    Fixing on Windows
# :construction:    Work in progress
# :construction_worker:    Adding CI build system
# :chart_with_upwards_trend:    Analytics or tracking code
# :heavy_minus_sign:    Removing a dependency
# :heavy_plus_sign:    Adding a dependency
# :whale:    Docker
# :wrench:    Configuration files
# :package:    Package.json in JS
# :twisted_rightwards_arrows:    Merging branches
# :hankey:    Bad code / need improv.
# :rewind:    Reverting changes
# :boom:    Breaking changes
# :ok_hand:    Code review changes
# :wheelchair:    Accessibility
# :truck:    Move/rename repository
# :wastebasket: remove unnecessary files
# :memo: memo memo

テンプレートを設定後、IntelliJさん側でコミットをしようとするとこんな感じでテンプレートのメッセージが適用されるようになります。

f:id:sioiri:20210310094235p:plain

会社用のみテンプレートを分けたいケース

私の場合はghqを使っているので、会社用のリポジトリが特定のディレクトリ配下に集まっています。 なので、includeIfを使って特定ディレクトリ配下のみ特定のテンプレートを適用する設定を入れています。

.gitconfigの設定はこんな感じにしています。(xxxxは組織名が入る感じですね)

[includeIf "gitdir:~/src/github.com/xxxx/"]
  path = ~/.xxxx-gitconfig

~/.xxxx-gitconfigに、コミットメッセージのテンプレートを指定する感じになります。

[commit]
  template = ~/xxxxCommitTemplate.txt

これで、会社用リポジトリのみテンプレートが適用できるようになりますね。

Testcontainersを使ったテストの高速化

Testcontainersを使ったテストは、コンテナの起動が毎回行われるのでどうしてもslow testになってしまいます。 そこで、一度あげたコンテナを使い回すことで2回目以降のテスト実行を高速化してみようと思います。

コンテナを使い回す設定を追加

コンテナを使い回す設定は、テストコードとTestcontainersの設定ファイルの両方に対して行う必要があります。 片方だけに設定を行っても有効にならないので注意です。

テストコード

テストコードで、コンテナを起動する際にreuse(org.testcontainers.containers.GenericContainer#withReuse)trueを設定してあげます。

    private val elasticsearchContainer: ElasticsearchContainer
    init {
        val time = measureNanoTime {
            elasticsearchContainer = ElasticsearchContainer("docker.elastic.co/elasticsearch/elasticsearch:7.11.1")
                .withCreateContainerCmdModifier {
                    it.withEntrypoint("/bin/bash", "-c", "./bin/elasticsearch-plugin install analysis-kuromoji && docker-entrypoint.sh")
                }
                .withLabel("filter-label", "plugin-install-test")
                .withReuse(true)
                .apply {
                    start()
                }
        }
        println("time >>>>> ${TimeUnit.NANOSECONDS.toMillis(time)}")
    }

Testcontainersの設定

上記のテストコードの設定に加えて$HOMEディレクトリ直下にある.testcontainers.propertiesに以下の設定を加えてあげます。 ファイルがない場合には、新規で作成して設定を追加する感じになります。

testcontainers.reuse.enable=true

実行結果

1回目と2回目の時間を比較してみると、コンテナ起動が省略でき、20秒ほど早くなっていることが確認できますね。

1回目

08:57:57.666 [Test worker] INFO 🐳 [docker.elastic.co/elasticsearch/elasticsearch:7.11.1] - Container docker.elastic.co/elasticsearch/elasticsearch:7.11.1 started in PT19.649672S
time >>>>> 21072

2回目

08:58:54.287 [Test worker] INFO 🐳 [docker.elastic.co/elasticsearch/elasticsearch:7.11.1] - Container docker.elastic.co/elasticsearch/elasticsearch:7.11.1 started in PT0.049485S
time >>>>> 1676

起動しっぱなしのコンテナの終了方法

Testcontainersで上げたコンテナンにはラベルが設定されているので、そのラベルでフィルターすることで簡単に終了できます。

コマンド的には、こんな感じになります。

docker ps --filter label=org.testcontainers -q | xargs docker stop

注意点

コンテナを使い回すことになるので、テスト開始時にクリーンな状態ではない可能性があります。 テストコードでは必ず前回のテストの状態を削除するなどして、きれいな状態にする必要があります。

TestcontainersのElasticsearch containerでコンテナ起動時にpluginをインストールする方法

TestcontainersのElasticsearchコンテナ起動時にテストで必要となるプラグインをインストールする方法を調べてみました。

サンプルコード

ElasticsearchContainerの親クラスのGenericContainerが、docker-java APICreateContainerCmdに対してなにか処理を追加できるwithCreateContainerCmdModifierメソッドを提供してくれています。 このメソッドを使って、entrypointを設定することでプラグインのインストールが実現できます。

サンプルコードでは、analysis-kuromojiインストール後にdocker-entrypoint.shを実行してElasticsearchを起動してみました。

import org.junit.jupiter.api.Test
import org.testcontainers.elasticsearch.ElasticsearchContainer

class ElasticSearchTest {

    val elasticsearchContainer = ElasticsearchContainer("docker.elastic.co/elasticsearch/elasticsearch:7.11.1")
        .withCreateContainerCmdModifier {
            it.withEntrypoint("/bin/bash", "-c", "./bin/elasticsearch-plugin install analysis-kuromoji && docker-entrypoint.sh")
        }
        .withLabel("filter-label", "plugin-install-test")
        .withReuse(true)
        .apply {
            start()
        }

    @Test
    internal fun test() {
        val containerId = elasticsearchContainer.containerId
        println("containerId = ${containerId}")
    }
}

pluginのインストール確認

withReusetrueにしているので、テスト終了後もコンテナが起動しっぱなしの状態となります。 そのコンテナ内に、入り込んでプラグインがインストールされていることを確認してみます。

docker ps --filter label=filter-label=plugin-install-test
CONTAINER ID   IMAGE                                                  COMMAND                  CREATED          STATUS          PORTS                                              NAMES
e705ae24a8a6   docker.elastic.co/elasticsearch/elasticsearch:7.11.1   "/bin/bash -c './bin…"   13 minutes ago   Up 13 minutes   0.0.0.0:49259->9200/tcp, 0.0.0.0:49258->9300/tcp   keen_gould
~ ❯ docker exec -it e705 bash
[root@e705ae24a8a6 elasticsearch]# ./bin/elasticsearch-plugin list
analysis-kuromoji

./bin/elasticsearch-plugin listの結果、 analysis-kuromojiが表示されたので想定通りプラグインがインストールされていますね!

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

おわり。