20190403のKotlinに関する記事は2件です。

ことりんと一緒 Springもね - 10. データベースマイグレーション - Flyway for MySQL

概要 / 説明

ことりんと一緒 Springもね - 9. データベースマイグレーション - Flyway では、Flywayを利用したデータベースレイアウトのバージョン管理の方法を確認しました。
そこで利用したデータベースは、作業の簡略化のために H2 Database を使用し、アプリケーションの起動とともに利用できるインメモリデータベースの状態で実施しました。

一方で実際にアプリケーションを稼働させる場合は、インメモリデータベースではなく、データベースサーバを立ち上げてドライバ経由で接続するケースがほとんどだと思います。
そこで、今回は MySQL をデータベースサーバとして起動し、それに対して Flyway を使用してみます。

flyway-logo-tm.png

前提 / 環境

ランタイムバージョン

  • Kotlin : 1.3.21
  • SpringBoot : 2.1.1.RELEASE

Spring Dependencies

  • Web
  • JDBC
  • JPA
  • Actuator
  • Flyway

開発環境

  • OS : Mac
  • IDE : IntelliJ IDEA
  • Build : Gradle

手順 / 解説

MySQL サーバの準備

MySQLサーバは Docker コンテナを利用して簡単に用意します、

docker-compose.yml の用意

次の環境が起動するようなdocker-compose.yml を作成します。

項目 内容
MySQL バージョン 5.7
アクセスユーザ guest
アクセスパスワード guest
version: "3.7"
services:
  db:
    image: mysql:5.7
    container_name: my_db
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_USER: guest
      MYSQL_PASSWORD: guest
      MYSQL_DATABASE: guest
    restart: always
    command: --default-authentication-plugin=mysql_native_password
    ports:
      - 3306:3306

MySQL サーバの起動

docker-compose.yml の配置場所、またはファイルを -f オプションで指定して、次のコマンドで Docker コンテナとして MySQL を起動します。

$ docker-compose up -d

Application 定義

以下のデータベース接続定義及びFlyway定義を追加します。

項目 設定値
JDBCドライバ com.mysql.cj.jdbc.Driver
データベース接続URL jdbc:mysql://localhost:3306/app?autoReconnect=true&useSSL=false
アクセスユーザ guest
アクセスパスワード guest
Flyway ベースラインバージョン 0.0.0
Flyway ベースライン説明 << Flyway Baseline >>
Flyway マイグレーション・スクリプトの配置場所 classpath:db/migration
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/app?autoReconnect=true&useSSL=false
    username: guest
    password: guest
  flyway:
    enabled: true
    url: jdbc:mysql://localhost:3306/app?autoReconnect=true&useSSL=false
    user: guest
    password: guest
    baseline-on-migrate: true
    baseline-version: 0.0.0
    baseline-description: << Flyway Baseline >>
    locations: classpath:db/migration

Flyway実行 (アプリケーション起動)

アプリケーションの起動時にFlywayを実行します。

SpringBoot 実行

以下のコマンドで SpringBoot を実行します。

$ ./gradlew bootRun

起動時ログから Flyway が正常実行されている事が確認できます。

[           main] o.f.c.internal.license.VersionPrinter    : Flyway Community Edition 5.2.4 by Boxfuse
[           main] o.f.c.internal.database.DatabaseFactory  : Database: jdbc:mysql://localhost:3306/app (MySQL 5.7)
[           main] o.f.core.internal.command.DbValidate     : Successfully validated 2 migrations (execution time 00:00.019s)
[           main] o.f.c.i.s.JdbcTableSchemaHistory         : Creating Schema History table: `app`.`flyway_schema_history`
[           main] o.f.core.internal.command.DbMigrate      : Current version of schema `app`: << Empty Schema >>
[           main] o.f.core.internal.command.DbMigrate      : Migrating schema `app` to version 1.0.0 - Create-InitialTable
[           main] o.f.core.internal.command.DbMigrate      : Migrating schema `app` to version 1.1.0 - Insert-InitialData
[           main] o.f.core.internal.command.DbMigrate      : Successfully applied 2 migrations to schema `app` (execution time 00:00.109s)

MySQL 確認

MySQLにアクセスしFlywayによる変更を確認します。

mysql> show tables;
+-----------------------+
| Tables_in_app         |
+-----------------------+
| flyway_schema_history |
| message               |
+-----------------------+
mysql> select id,title,message from message;
+--------------------------------------+-------+--------------------+
| id                                   | title | message            |
+--------------------------------------+-------+--------------------+
| 12345678-e9d9-4d1e-ba79-01f8b8715ba9 | INIT  | Inserted by FLYWAY |
| 7b23257c-e9d9-4d1e-ba79-01f8b8715ba9 | INIT  | Inserted by FLYWAY |
+--------------------------------------+-------+--------------------+
2 rows in set (0.00 sec)

message テーブルと、Flyway の履歴管理テーブル flyway_schema_history が作成されている事が確認でき、
データが追加されている事も確認できました。

まとめ / 振り返り

組み込みモードでもサーバモードでも起動しているデータベースに対してアクセスする事ができれば、Flywayが使用できる事が確認できました。
データベースレイアウトに変更が発生しがちな開発中のタイミングでは非常に便利なツールだと思います。

今回のソース

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Kotlin の Suspending 関数を Mockito で spy する

mockito-kotlin を使って Suspending 関数を含むクラスのテストを書いていたところ、現状ではちょっとしたコツが必要なことが分かった。

以下の DownloadService#downloadFile() を呼び出す別のメソッドをテストするため、このインターネットと通信する downloadFile() メソッドをモックに置き換えようとした。

mock() を使った以下のコードは正常に実行可能。

class DownloadService {
    suspend fun downloadFile(url: String): File {
        // 省略
    }

    // 実際には別のメソッドも定義されている
}

@Test fun testDownloadFile() {
    val file = createTempFile()
    val target = mock<DownloadService>() {
        onBlocking{ downloadFile(any()) } doReturn file
    }
    runBlocking {
        val downloadedFile = target.downloadFile("http://example.com")
        assertEquals(file, downloadedFile)
    }
}

実際には DownloadService クラスに含まれる downloadFile() メソッド以外はモックを使わずにそのまま実行されて欲しかったので、mock()spy() に置き換え、このメソッドだけをモック実装する形に。

@Test fun testDownloadFile() {
    val file = createTempFile()
    val target = spy<DownloadService>() {
        onBlocking{ downloadFile(any()) } doReturn file
    }
    runBlocking {
        val downloadedFile = target.downloadFile("http://example.com")
        assertEquals(file, downloadedFile)
    }
}

ところが、このテストコードを実行すると NullPointerException がスローされてしまう(4 行目で実行される Stub 処理中に)。

mock() はいいけど spy() だとダメ。
しかしながら、このように spy() を使ってもモック対象(今回の例で言うと downloadFile() メソッド)が引数を取らない場合は、NPE がスローされることなくテストを実行できることが分かった。

試行錯誤した結果、spy() にラムダ式でモック処理を渡すのではなく、次のように doReturn().whenever().xxxx() のコードでモック処理を書けば正常にテストが実行できるようになった。

@Test fun testDownloadFile() = runBlocking {
    val file = createTempFile()
    val target = spy<DownloadService>()
    doReturn(file).whenever(target).downloadFile(any())
    val downloadedFile = target.downloadFile("http://example.com")
    assertEquals(file, downloadedFile)
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む