読者です 読者をやめる 読者になる 読者になる

a fledgling

駆け出しが駆け出してみる

Kotlin+SpringBootでWebアプリを作ってみる その3

やること

Kotlin+SpringBootでWebアプリを作ってみる その2.1の続きです。

結構間が空いてしまったな...。

今回は、

・Flywayの導入
DBUnitの導入

をやります。というかやりました。

前回の終わりに書いた次やることとは全然違うけど気にしない。
次で書きます。

ソースはGitHubにあげてあります。

Flywayの導入

ちょっとやってみたかったので、DBマイグレーションツールのFlywayを導入します。

DBマイグレーションツールってのはデータベースの状態をバージョン管理するやつですね。
DB環境生成や移行がやりやすくなったりします。どんな順番でSQLを実行するのかとか、パッチ用SQLを適用してるのかとか全部マイグレーションツールがやってくれます。

仕事でも使いたいですね。

とりあえず現状あるテーブルを管理対象としてやってみました。
かなり簡単です。

build.gradle

dependencies {
    ...
    compile('org.flywaydb:flyway-core:4.0')
}

依存関係にFlywayを追加しました。バージョンは4.0。
たぶん一番新しいreleaseだと思います。
以上。

V1__createSchema.sql

DROP TABLE IF EXISTS author_book;
DROP TABLE IF EXISTS category_book;
DROP TABLE IF EXISTS publisher_book;
DROP TABLE IF EXISTS book;
DROP TABLE IF EXISTS author;
DROP TABLE IF EXISTS category;
DROP TABLE IF EXISTS publisher;

CREATE TABLE IF NOT EXISTS `book` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `image_path` varchar(255) NOT NULL,
  `leading_sentence` varchar(255) NOT NULL,
  `sub_title` varchar(255) DEFAULT NULL,
  `title` varchar(255) NOT NULL,
  `url` varchar(255) NOT NULL,
  `like_count` int(11) NOT NULL DEFAULT '0',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE IF NOT EXISTS `author` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) NOT NULL,
  `like_count` int(11) NOT NULL DEFAULT '0',
  PRIMARY KEY (`id`),
  UNIQUE KEY `name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE IF NOT EXISTS `category` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) NOT NULL,
  `like_count` int(11) NOT NULL DEFAULT '0',
  PRIMARY KEY (`id`),
  UNIQUE KEY `name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE IF NOT EXISTS `publisher` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) NOT NULL,
  `like_count` int(11) NOT NULL DEFAULT '0',
  PRIMARY KEY (`id`),
  UNIQUE KEY `name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE IF NOT EXISTS `author_book` (
  `author_id` bigint(20) NOT NULL,
  `book_id` bigint(20) NOT NULL,
  PRIMARY KEY (`author_id`,`book_id`),
  KEY `book_id` (`book_id`),
  CONSTRAINT `author_book_ibfk_1` FOREIGN KEY (`author_id`) REFERENCES `author` (`id`),
  CONSTRAINT `author_book_ibfk_2` FOREIGN KEY (`book_id`) REFERENCES `book` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE IF NOT EXISTS `category_book` (
  `category_id` bigint(20) NOT NULL,
  `book_id` bigint(20) NOT NULL,
  PRIMARY KEY (`category_id`,`book_id`),
  KEY `book_id` (`book_id`),
  CONSTRAINT `category_book_ibfk_1` FOREIGN KEY (`category_id`) REFERENCES `category` (`id`),
  CONSTRAINT `category_book_ibfk_2` FOREIGN KEY (`book_id`) REFERENCES `book` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE IF NOT EXISTS `publisher_book` (
  `publisher_id` bigint(20) NOT NULL,
  `book_id` bigint(20) NOT NULL,
  PRIMARY KEY (`publisher_id`,`book_id`),
  KEY `book_id` (`book_id`),
  CONSTRAINT `publisher_book_ibfk_1` FOREIGN KEY (`publisher_id`) REFERENCES `publisher` (`id`),
  CONSTRAINT `publisher_book_ibfk_2` FOREIGN KEY (`book_id`) REFERENCES `book` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

Flywayでは、src/main/resources/db/migration以下のSQLファイルをDBへの適用対象とします。
バージョンごとにSQLファイルを作って管理をするので、ファイル名に規則がありバージョン番号は一意でないとだめです。まぁ当たり前か。

ファイル名はVからはじめて、バージョン番号(数字と._が使える)、区切り文字の__(アンスコ2つ)、説明。

SQLファイルの中は普通にSQL書くだけです。
マイグレーションを実行するとDBに未適用のバージョンのSQLが実行され、適用済みのSQLはFlywayが判断してスキップしてくれます。

Flywayはこれで終わりです。簡単。
これだけでDBが変わっても自動で環境を作ってくれます。

今回はテーブルの作成しか行っていないですが、データのインサートや構造の変更ももちろんできます。
そのへんはSpring Securityを導入した際に使ってみます。

DBUnitの導入

そろそろぐだぐだなテストをなんとかしようと思って、導入しました。
DBUnitはデータベースを操作するクラスのテストをするためのフレームワークですね。

仕事でも使いたい。

CSVファイルかxmlファイル(xlsファイルも?)でデータを作成して、テスト実行時に必要なデータを入れたり消したりしてくれます。
毎回手動でデータを用意しなくても大丈夫なので、テストを何度でも気軽に実行できるようになります。神。

それにSpring Bootはsrc/test/resources/直下にapplication.ymlapplication.propertiesを置くとテスト時はそちらを読んでくれるので、datasourceを簡単に切り替えられます。
加えて、上記でFlywayを導入したのでテーブルの生成などなども自動でやってくれるのでテストやりたい放題ですね。ほんとに仕事で使いたい。

とりあえずbuild.gradleから書いていこう。

build.gradle

dependencies {
    ...
    testCompile('org.dbunit:dbunit:2.5.1')
}

依存関係にDBUnitを追加。以上。

application.yml

spring:
# DataSource
  datasource:
    url: jdbc:mysql://localhost/book_application_test
    username: user
    password: user
    driver-class-name: com.mysql.jdbc.Driver
# JPA
  jpa:
    show-sql: true
    hibernate:
      ddl-auto: validate

テスト用のdatasourceを書いてsrc/test/resources/直下に。
これでテスト時はbook_application_testを読みに行ってくれます。

そういえばhibernate.ddl-autoDDLをFlywayがやってくれるのでvalidatenoneでいいと思います。

TestDataResources.kt

package book.application.test

import org.dbunit.database.DatabaseConnection
import org.dbunit.dataset.csv.CsvDataSet
import org.dbunit.operation.DatabaseOperation
import org.junit.rules.ExternalResource
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Component
import java.io.File
import javax.sql.DataSource

@Component
class TestDataResources : ExternalResource() {
    @Autowired
    lateinit private var dataSource: DataSource

    override fun before() {
        var con: DatabaseConnection? = null
        try {
            // テストデータのインサート
            con = DatabaseConnection(dataSource.connection)
            DatabaseOperation.CLEAN_INSERT.execute(con!!, CsvDataSet(File("src/test/resources/testData")))
        }finally{
            con?.close()
        }
    }

    override fun after() {
        // 特にやることがないな...
    }
}

今回はJUnit@Ruleアノテーションを使います。なのでそれ用のクラスを作成しました。
@ComponentBean登録して、テストクラスにAutowiredします。

やってることは見た通り、テスト実施前にインサートをするだけです。
CLEAN_INSERTを使うことで、DELETE_ALLINSERTを一括で行ってくれます。
テスト用のDBなので毎回データを消しても問題ないのでこれを使います。

executeメソッドの第二引数にデータを記述したファイルのあるパスを設定したDataSetを渡すと、パス直下のファイルの内容をインサートしてくれます。
今回はCSVファイルで作ったのでCsvDataSetを使っています。

YAMLでも頑張ればいけそうな気がしたんですが、YAMLで同じ構造のデータを何度も書くのってどうなんだろと思ったのでやめました。

XMLファイルは見にくい書きにくいで出来れば書きたくないし、Excelは毎日格闘してるので触りたくないしで、もうCSVしかないじゃんって感じですね。

BookServiceTest.kt

package book.application.service

import book.application.BookApplication
import book.application.test.TestDataResources
import org.junit.Assert.*
import org.junit.BeforeClass
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.SpringApplicationConfiguration
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner
import org.springframework.test.context.web.WebAppConfiguration
import javax.transaction.Transactional
import org.hamcrest.CoreMatchers.`is` as be

@RunWith(SpringJUnit4ClassRunner::class)
@SpringApplicationConfiguration(BookApplication::class)
@WebAppConfiguration
open class BookServiceTest{
    @Rule
    @Autowired
    lateinit var testDataResources: TestDataResources

    @Autowired
    lateinit var bookService: BookService

    @Test
    @Transactional
    fun findAllBookTest() {
        var sutList = bookService.findAllBook()
        var sut = sutList[0]
        assertThat(sutList.size, be(10))
        assertThat(sut.id, be(1L))
        assertThat(sut.title, be("title"))
        assertThat(sut.subTitle, be("subTitle"))
        assertThat(sut.leadingSentence, be("leadingSentence"))
        assertThat(sut.imagePath, be("imagePath"))
        assertThat(sut.url, be("url"))
        assertThat(sut.authors[0].name, be("author"))
        assertThat(sut.likeCount, be(0))
        assertThat(sut.categories[0].name, be("category"))
        assertThat(sut.publisher.name, be("publisher"))
    }

}

改変したテストクラス。@Ruleに上記のTestDataResourcesAutowiredしたくらいであんまり変わってないです。
でもこれで毎回データを用意しなくてもテストが実行可能になりました。

テストが自動で行えるってことはリファクタリングできるってことですからね、最高ですね。

あとはCSVにデータを書いていくだけです。冗長なので、ここまで飛ばしてもらっても。

csvファイルたち

book.csv
id,title,sub_title,leading_sentence,image_path,url,like_count
1,"title","subTitle","leadingSentence","imagePath","url",0
2,"title2","subTitle2","leadingSentence2","imagePath2","url2",0
3,"title3","subTitle3","leadingSentence3","imagePath3","url3",0
4,"title4","subTitle4","leadingSentence4","imagePath4","url4",0
5,"title5","subTitle5","leadingSentence5","imagePath5","url5",0
6,"title6","subTitle6","leadingSentence6","imagePath6","url6",0
7,"title7","subTitle7","leadingSentence7","imagePath7","url7",0
8,"title8","subTitle8","leadingSentence8","imagePath8","url8",0
9,"title9","subTitle9","leadingSentence9","imagePath9","url9",0
10,"title10","subTitle10","leadingSentence10","imagePath10","url10",0
author.csv
id,name,like_count
1,"author",0
2,"author2",0
3,"author3",0
4,"author4",0
5,"author5",0
6,"author6",0
7,"author7",0
8,"author8",0
9,"author9",0
10,"author10",0
category.csv
id,name,like_count
1,"category",0
2,"category2",0
3,"category3",0
4,"category4",0
5,"category5",0
6,"category6",0
7,"category7",0
8,"category8",0
9,"category9",0
10,"category10",0
publisher.csv
id,name,like_count
1,"publisher",0
2,"publisher2",0
3,"publisher3",0
4,"publisher4",0
5,"publisher5",0
6,"publisher6",0
7,"publisher7",0
8,"publisher8",0
9,"publisher9",0
10,"publisher10",0
author_book.csv
author_id,book_id
1,1
2,2
3,3
4,4
5,5
6,6
7,7
8,8
9,9
10,10
category_book.csv
category_id,book_id
1,1
2,2
3,3
4,4
5,5
6,6
7,7
8,8
9,9
10,10
publisher_book.csv
publisher_id,book_id
1,1
2,2
3,3
4,4
5,5
6,6
7,7
8,8
9,9
10,10

うん、冗長。

それはさておき、DBUnitではファイル名でテーブル名を、一行目でカラム名を指定します。
あとはデータを詰めるだけ。

ちなみにauto_incrementdefaultを指定しているようなカラムでもデータを空にしておくとエラーを吐きます。
カラム自体を書かなかったらうまくいくのかもしれません。(試してない)

table-order.txt
book
author
category
publisher
author_book
category_book
publisher_book

このファイルでデータをインサートする順番を制御できます。
データファイルと同じディレクトリ(今回だとsrc/test/resources/testData/直下)に置くとDBUnitが読んで、順に実行してくれます。

このアプリケーションでいうと、author_bookテーブルにはbookテーブルと対応するauthorテーブルのidしか入らないので、先にbook,authorテーブルにデータをインサートしないといけません。
そういう順序の制御をします。

これでDBUnitの導入も終了です。割と簡単。  

所感

テスト自動化は神。
テストコードを書こう。

次やること

次こそページネーション。
あとはSpring Securityを入れます。

もうやってあるんですけどね...。