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.yml
かapplication.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-auto
はDDLをFlywayがやってくれるのでvalidate
かnone
でいいと思います。
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
アノテーションを使います。なのでそれ用のクラスを作成しました。
@Component
でBean
登録して、テストクラスにAutowired
します。
やってることは見た通り、テスト実施前にインサートをするだけです。
CLEAN_INSERT
を使うことで、DELETE_ALL
とINSERT
を一括で行ってくれます。
テスト用の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
に上記のTestDataResources
をAutowired
したくらいであんまり変わってないです。
でもこれで毎回データを用意しなくてもテストが実行可能になりました。
テストが自動で行えるってことはリファクタリングできるってことですからね、最高ですね。
あとは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_increment
やdefault
を指定しているようなカラムでもデータを空にしておくとエラーを吐きます。
カラム自体を書かなかったらうまくいくのかもしれません。(試してない)
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を入れます。
もうやってあるんですけどね...。