a fledgling

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

GWに読む本を検討する

GWに東京に来てから初めて関西に帰る。
んだけど、多分暇だからGW中にこれらを読破するってのを決めようと思う。

気になるのがあったら追加していこ。

Spring BootでRequest Bodyをログに出力する

API serverをSpring Bootで作っていて、リクエストボディ(JSON文字列)をログに吐こうとしてちょっと悩んだのでメモ。

問題

Interceptorで実装しているロギング処理でリクエストボディを取得しようとすると以下の問題にぶつかった。

  1. HttpServletRequest#getReaderIllegalStateExceptionを投げる
  2. HttpServletRequest#getInputStream#readLineとかで読むと、Controllerでリクエストボディがないと言われる
  3. ServletInputStreamresetに対応していない

辛い。

解決策

ググってると、HttpServletRequestをラップして、リクエストボディを保持しておいて、そこから都度InputStreamを作ればええねん! ってのを見つけたので、実装。例によってKotlinで書いている。

private class MyServletInputStream(bytes: ByteArray) : ServletInputStream() {
    private val inputStream: ByteArrayInputStream = ByteArrayInputStream(bytes)
    
    override fun read(): Int = inputStream.read()
    
    override fun read(b: ByteArray?, off: Int, len: Int): Int {
        return inputStream.read(b, off, len)
    }

    override fun isReady(): Boolean = inputStream.available() != 0

    override fun setReadListener(listener: ReadListener?) {}

    override fun isFinished(): Boolean = inputStream.available() == 0
}

private class MyServletRequestWrapper(request: HttpServletRequest) : HttpServletRequestWrapper(request) {
    private val bytes: ByteArray = ByteArrayOutputStream().use {
        request.inputStream.copyTo(it)
        it.toByteArray()
    }

    override fun getInputStream(): ServletInputStream {
        return MyServletInputStream(this.bytes)
    }
}

ServletInputStreamの実装クラスのread以外のメソッドは適当に書いた。

どう使うか

  1. Interceptorより前に処理が走るFilterで、HttpServletRequstMyServletRequestWrapperでラップして、FilterChain#doFilterの引数として渡す。
  2. Interceptor内で、HttpServletRequest#getInputStream#readLineとかで読んでログに吐く
    • このHttpServletRequestの実態はMyServletRequestWrapper

終わり

JetBrains IDE Supportの警告を消す方法

Chrome ExtentionにJetBrains IDE Supportというものがあります。

リアルタイムでHTML/CSS/JavaScriptがブラウザに反映されてデバッグまでできるなかなか良い拡張機能です。
なのですが、デフォルトだとChromeのバナーの下に黄色い警告が出ます。

f:id:KissyBnts:20160916235514p:plain

なかなか邪魔だ。
ということで
chrome://flags/#silent-debugger-extension-api
をアドレスバーにコピペして、遷移先にあるサイレントデバッグっていうのを有効にすると消えてくれた。
これでまた一つIntelliJ IDEAが快適になった。めでたしめでたし。

HomebrewでNode.jsを入れる方法とエラーの解決方法

新しいMacNode.jsを入れた際の手順メモ

Node.js, Nodebrewを削除

$ brew uninstall node
$ brew uninstall nodebrew

Nodebrewのインストール

$ brew install nodebrew

PATHを設定

$ echo 'export PATH=$HOME/.nodebrew/current/bin:$PATH' >> ~/.bashrc

PATHの反映のため再読み込み
$ source ~/.bashrc

安定版のNode.jsのインストール

$ nodebrew install-binary stable

をするとエラーになった。以下の通り。

curl: (23) Failed writing body (0 != 941)
download failed: https://nodejs.org/dist/v6.5.0/node-v6.5.0-darwin-x64.tar.gz

ググった結果、ディレクトリがないらしいので作成
$ mkdir -p ~/.nodebrew/src

再度Node.jsのインストール
$ nodebrew install-binary stable

Fetching: https://nodejs.org/dist/v6.5.0/node-v6.5.0-darwin-x64.tar.gz
...
Installed successfully

OK!

使うバージョンを指定

$ nodebrew use stable

インストールされていることを確認
$ node -v

バージョンが表示されたら完了

PATHを設定

$ echo 'export PATH=$PATH:/Users/USER_NAME/.nodebrew/current/bin' >> ~/.bashrc

炎上プロジェクトを振り返る

自分の備忘録というか戒めです。

9月から東京のとある会社で働く。

つまり前の会社を辞めた訳んだけど、
最後に関わった改修案件がよく燃える案件だったので何がダメだったのか・どうすべきだったかを振り返っておこうと思う。

概要

  • 既存業務システムの機能追加
  • 3月スタート、8月納品だった
  • 一つ一つは大したことがないが、数が多く全体でみると大きな案件だった
  • メンバーは10人くらい(自分とリーダーがメイン)
  • Java6, Struts, ibatis, freemarker, JavaScript(pure)など
  • ドキュメントはもちろんExcel

何が起きたか

  • 大量の設計漏れ・仕様追加/変更による実装工程の遅れ
  • 試験書が仕様通りでないのでスムーズに試験ができない
  • 当たり前だけど不具合の頻出
  • 再三のデグレ
  • 追い討ちの追加対応

オワタ... orz

と言ってても仕方がないので、同じような案件に巻き込まれないようにミスを犯さないように一つずつ振り返る。

大量の仕様漏れ・仕様追加/変更による実装工程の遅れ

仕様追加・変更については、お客さんとの折衝はしていないのでちょっとアレな部分があるが、やはり設計段階で詰めるべきだった仕様についてが多かった。
また、実装段階で気づいて決めた仕様が多すぎた。
そして設計不十分の影響は最後の最後までこの案件に大きな影を落とすことに。

これに関しては設計力の不足を思い知った。
業務知識に寄る部分が大きかったが、なんとなくわかったような気がしたくらいで設計をしていたような気がする。まぁこれでいけるやろ的な。
完全に漏れ無く完璧には無理かもしれないけど、ウォーターフォールで進めていた以上、もっと精度を上げていかないとお話にならない感。今後ウォーターフォールでやるかは知らない

チーム的な問題としては、業務知識を蓄えている人(=同じ現場にいる年数が長い人)がリーダー一人しかいなかったこと。
そして、その人に設計書・試験書のレビューが集中してしまったことがある。
一番わかっている人がレビューをするのは理にかなっていると思うけど、今回は数が多すぎたため一人で見切ろうとするとどうしても漏れが出る。
別チームの人でも、なんとかお願いして作業分担をしてもらうべきだった。
本当は自分自身が出来ると良かったのだけど、業務経験は1年くらいで、今回改修する部分については今までほとんど触ったことがなかった部分だった。
とはいえ、レビューなんかできませんなんてはずはなかったので、比較的業務知識に寄らない部分の作業の切り出しを自分から行うべきだったと思う。

試験書が仕様通りでないのでスムーズに試験ができない

設計書作成 → 試験書作成 → 実装 → 単体試験 → 結合試験
というフローで進めていたが、実装段階で仕様をいじりまくったおかげで、実装(=仕様)と試験書と設計書がそれぞれ乖離する状況になってしまった。
また、試験の少し前くらいにメンバーが諸事情で離脱。ほとんど経験のない人にテスターをお願いすることに。
試験書の通りに操作してもその通りの結果にならないし、設計書を見てもまた違うことが書いてあるしと、大混乱。テスターの不満も不具合件数も山積み...。
また、ある程度業務知識がある人が試験をする前提で試験書を作ることが通例となっていたため、ことあるごとにテスターの方に説明をしなければならず実装者もテスターも時間を奪われるという最悪の状況。
さらに上がった不具合から、仕様通りの動作なのか不具合なのかを実装者が都度判断しなければならず、見積もりを立てた時点で積んでいたバッファは単体試験の工程で無くなり利益を食う状況に。

これで悪かったのは、実装段階で追加・変更となった仕様をドキュメントに反映していなかったこと。そのルール・仕組みがなかったこと。
仮に、実装段階で見つけた仕様漏れや、仕様の追加・変更は設計書・試験書に反映してから実装を行わなければならないというルールが決まっていて、メンバー全員が理解していたなら、実装工程はもう少し遅れたかもしれないけども、その後の混乱や無駄な時間の発生は防げた。
でもどうやったら効率的なその仕組みが作れるかは思い至っていないところ。

こういうトラブルや課題が発生した時にどういう対処を取るかっていうのは、チームメンバーで意識を合わせておかないといけない部分だったんだろうなと。

ある意味納得意外だったのは、もっと誰でも試験できるような試験書を作るべきだったのでは?っていう意見を出したらテスターの方以外から賛成が得られなかったこと。
そんなにコストのかかるようなものではない気がするんだけど、今までのを変えるのはなかなか受け入れられないのだなと...。

当たり前だけど不具合の頻出

まぁ、上の2つを鑑みると当たり前。 もう一つの原因としては、Javaをメインで仕事をしている人たちばかりのチームだったのに、JavaScriptを使ってフロントでごちゃごちゃやりすぎたこと。

諸般の事情によりあまりプラグイン拡張機能を使えない状況で作業をしていたので、変数や関数のタイポなんてしょっちゅう。
そんな状況で1000行とか書くもんだから何が何やら、書いた本人でさえ理解できない状況に。しかも中には昔のコードからコピペしたようなものもあって、そのコードの質も良くない...。

そんなわけで、設計段階でこの処理はフロントで、これはサーバーでと設計をするわけだけども、チームのスキルの志向にあった設計をしなければ泣きを見ることがわかったよね。
もちろん、機能面でこれはフロントで、サーバーでやらなければいけないっていうのはあるので、そこは仕方ないけれどできるだけチームのスキルセットにあった設計をしよう。

再三のデグレ

不具合修正によるデグレ、不具合修正したソースに追加対応したソースをマージした際のデグレ、いつ起きたのかわからないデグレ...。
その度に手動による再試験。そこで別の不具合やデグレを見つけてまた再試験。その繰り返し。インフィニティ。

もうテストコード書けよとしか。
そしてなぜ自分は途中でテストコードを書くのをやめてしまったのか。
UIのテストまで書けとは言わないけど、大事なロジックのテストコードぐらい書いておけよ。
それで見つかる不具合・デグレがどれだけあったか。

確かに、テストコードを書くような工数はお金はもらっていないから書かないっていうのはわかるんだけど、それはある程度までの規模の小さな案件の場合に有効で、今回のように全体として大きい場合はお金をもらえなくても書いたほうが時間を回収できるし、利益が上がる。
お客さん的にも不具合件数が下がって、信頼できるようになるはず。

今回、一人でもテストコードを書くスタイルを崩すべきではなかった。
そうすれば、自分の書いた部分だけでも不具合・デグレは少なくなって、他の部分に割ける時間が増えただろうに。

ちなみに一人で書いてたのは誰もJUnitの書き方を知らなかったから。

追い討ちの追加対応

これはもう本当に勘弁してとしか...。
お客さんも自分たちも幸せになれないものに対しては、断る勇気も持とう。

最後に

もう全行程ダメダメで、二度とやりたくない。本当に。

でも設計段階で少し抜いてたのが全ての始まり。
不具合が多発した時も、それぞれにプライオリティ付けて処理をしなかったのが悪い。
影響範囲を対して検討もせずにその場しのぎな方法で直してたが悪い。
ドキュメントを軽視していたのが悪い。

同じことは繰り返さないようにしよう。
以上。

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を入れます。

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

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

発端

Kotlin+SpringBootでWebアプリを作ってみる その2でテストコードでLazyInitializationExceptionが出てはまったって話を書きました。

すると、槇さんがSpringの永続コンテキストのライフサイクルを教えてくださりました。
ありがたい... ということで書いておこうと思います。

Springにおける永続コンテキストの話

以下槇さんからいただいたツイート

なるほど…。Spring Bootの庇護下になかったからなのか…。

つまりBookService.findAllBook()の呼び出し終了時にEntityManagerをSpringが閉じてしまってるから、遅延初期化でエラーを吐いていたのですね。
で、@TransactionalメソッドにアノテートすることでEntityManagerが閉じるのをメソッド単位にすると。するとメソッド内で遅延アクセスをしても問題無し。

ちなみに@Transactionalをクラスもしくはメソッドに付与するとfinalやめろエラーが吐かれるのでopenをつけましょう。

BookServiceTest.kt

package book.application.service

import book.application.BookApplication
import org.junit.Assert.*
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{
    @Autowired lateinit var bookService: BookService

    @Test
    @Transactional
    fun findAllBookTest() {
        var sutList = bookService.findAllBook()
        var sut = sutList[0]
        assertThat(sutList.size, be(1))
        assertThat(sut.id, be(1L))
        assertThat(sut.title, be("title"))
        assertThat(sut.subTitle, be("subTitle"))
        assertThat(sut.leadingSentence, be("leading"))
        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"))
    }
}

@Transactionalをクラスにアノテートしても、メソッドにアノテートしても動作します(入れ知恵)。

読もう

所感

Spring Bootがやってくれてるから問題ないけど、わかってないことたくさんあるんだろうなぁって感じですね。
これからもちょくちょくはまりそうだな…。

にしても、わからないなーと思ってた部分を拾っていただけて教えていただけるなんて、ほんとにありがたいことです。
もっとインプット・アウトプットして少しでも恩を返せるようになれろうと思いながら書いていました。

槇さん、ありがとうございました!
そして僕は明日の仕事終わりにはじめてのSpring Bootを買いに行こうと決めました…。

GitHubにRepository作った

ここです。以上。