ScalikeJDBCの自動生成テストの通し方・DB切替してテストする方法

# 0.前座

ScalikeJDBCを使ってのMySQL操作で遊んでみました。
Play Frameworkのプラグインもあり、導入コストもかなり低く重宝しています。
導入の仕方についての記事は、他の方々がよりよい感じで書いてくれてるとは思うので割愛。

プロダクトコードだと、テストでは本番のDBにつないでほしくない！
そういった方のための記事です。

設定はこんな感じです。

```scala:build.sbt
//前略

scalaVersion := "2.11.4"

//中略

libraryDependencies ++= Seq(
  "org.scalikejdbc" %% "scalikejdbc"                     % "2.2.0",
  "org.scalikejdbc" %% "scalikejdbc-config"              % "2.2.0",
  "org.scalikejdbc" %% "scalikejdbc-test"                % "2.2.0"  % "test",
  "org.scalikejdbc" %% "scalikejdbc-play-plugin"         % "2.3.2",
  "org.scalikejdbc" %% "scalikejdbc-play-fixture-plugin" % "2.3.2",
  "mysql"           %  "mysql-connector-java"            % "5.1.33",
  //その他ライブラリは省略
)

//コードジェネレータ設定を有効化
scalikejdbcSettings
```

```scala:project/plugins.sbt
//mysql
libraryDependencies += "mysql" % "mysql-connector-java"  % "5.1.33"

//DBコード自動生成
addSbtPlugin("org.scalikejdbc" %% "scalikejdbc-mapper-generator" % "2.2.0")
```

```scala:conf/play.plugins

10000:scalikejdbc.PlayPlugin
11000:scalikejdbc.PlayFixturePlugin

```

```scala:conf/application.conf
# Database configuration
db.default.driver="com.mysql.jdbc.Driver"
db.default.url="jdbc:mysql://localhost:3306/hoge"
db.default.user="hogeUser"
db.default.password="hogePassWord"

# Connection Pool settings
db.default.poolInitialSize=10
db.default.poolMaxSize=20
db.default.poolConnectionTimeoutMillis=1000

```


# 1.scalikejdbc-gen直後だと動かない！？

ScalikeJDBCのプラグインscalikejdbc-genは非常に楽で、コマンド一発でテスト書いてくれます。

```shell-session
$ #play Framework 2.3で試したので、SBT使う人はsbtと読み替えてくださいね）
$ ./activator "scalikejdbc-gen [TableName]"
```
が、このままだとTest - Failもしくはerrorしてしまいます。
なぜ動かないのかを説明こみで、動くテストをつくっていきましょう。

```Scala:src/test/scala/models/SampleSpec.scala編集前
package models

import scalikejdbc.specs2.mutable.AutoRollback
import org.specs2.mutable._
import org.joda.time._
import scalikejdbc._

class SampleSpec extends Specification {

  "Sample" should {

    val s = Sample.syntax("s")

    "find by primary keys" in new AutoRollback {
      val maybeFound = Sample.find(1L)
      maybeFound.isDefined should beTrue
    }
    "find all records" in new AutoRollback {
      val allResults = Sample.findAll()
      allResults.size should be_>(0)
    }
    "count all records" in new AutoRollback {
      val count = Sample.countAll()
      count should be_>(0L)
    }
    "find by where clauses" in new AutoRollback {
      val results = Sample.findAllBy(sqls.eq(s.id, 1L))
      results.size should be_>(0)
    }
    "count by where clauses" in new AutoRollback {
      val count = Sample.countBy(sqls.eq(s.id, 1L))
      count should be_>(0L)
    }
    "create new record" in new AutoRollback {
      val created = Sample.create(id = 1L, name = "MyString")
      created should not beNull
    }
    "save a record" in new AutoRollback {
      val entity = Sample.findAll().head
      // TODO modify something
      val modified = entity
      val updated = Sample.save(modified)
      updated should not equalTo(entity)
    }
    "destroy a record" in new AutoRollback {
      val entity = Sample.findAll().head
      Sample.destroy(entity)
      val shouldBeNone = Sample.find(1L)
      shouldBeNone.isDefined should beFalse
    }
  }

}
```

## コンフィグの初期化が存在しない！
* DBs.setupAllを実行する。

テストはひとつのテスト内で完結することが望ましいので、できれば1つ1つに書くほうがいいとかんがえますが、
何度も書くとめんどくさいのでコンストラクタで書いてしまえばいいと思います。 

```scala:コンフィグ初期化
scalikejdbc.config.DBs.setupAll //コンフィグのセットアップ

//Append DB切り替えでコンフィグが決まってる場合にはこちらでも。
//scalikejdbc.config.DBs.setup('test) //db.test.* の読み込みのみの場合はこちら
```

## アップデートがアップデートしていないテストに！

よくよくみるとアップデートしてないやーん！というテストになってます。

```scala:アップデート変更前
"save a record" in new AutoRollback {
  val entity = Sample.findAll().head
  // TODO modify something
  val modified = entity
  val updated = Sample.save(modified)
  updated should not equalTo(entity)
}
```

なので、アップデートさせましょう

```scala:アップデート変更後
"save a record" in new AutoRollback {
  val entity = Sample.findAll().head
  val modified = entity.copy(name = "Changed")
  val updated = Sample.save(modified)
  updated should not equalTo (entity)
}
```

## 空のテーブルだと1行目がないからうごかない

当たり前ですよね；レコードのないテーブルでfindしても…という話です。
ただ、テストのためだけに1行追加するのもバカバカしい。

### AutoRollBackとFixtureを使おう！

ScalikeJDBCはテストにおいても非常に優れており、AutoRollBackとFixtureという機能があります。
それを使ってコードを追加します。

やり方は簡単で、AutoRollbackを継承して「fixture」をオーバーライドします。

```scala:SampleAutoRollbackWithFixture
trait SampleAutoRollbackWithFixture extends AutoRollback {
   override def fixture = {
      //ダミー用のDB作成コードを書く
      SQL("insert into sample values (?, ? ,?)").bind(1, "MyString", "http://test.com").update.apply()
   }
}
```

そして、今まで new AutoRollbackとしてたところを新しく作ったTraitに変更します。

```Scala:
//"find by primary keys" in new AutoRollback {
"find by primary keys" in new SampleAutoRollbackWithFixture {
//省略
```


## 主キーが重複

上記で作成した自前のAutoRollbackを使用すると主キー重複でテストがこけてしまうので、追加するレコードと異なる主キーの値を設定します。
（このテストのみ、自前のAutoRollbackを使用しないのも手です）

```Scala:クリエイト（インサート）変更前
"create new record" in new SampleAutoRollbackWithFixture {
  val created = Sample.create(id = 1L, name = "MyString")
  created should not beNull
}
```

```Scala:クリエイト（インサート）変更前
"create new record" in new SampleAutoRollbackWithFixture {
  val created = Sample.create(id = 2L, name = "MyString")
  created should not beNull
}
```


# 3. DBを切り替えよう！

## テスト用のDB設定を追加

ScalikeJDBCを調べているとDBの切り替え方はテストで使用できそうなDBの切り替え方は2種類あるっぽいので両方記載します。

### Aパターン

(任意名).db.default.* で定義する方法

```scala:conf/application.conf（Aパターン）
# テスト用サンプルA
# Database configuration
test.db.default.driver="com.mysql.jdbc.Driver"
test.db.default.url="jdbc:mysql://localhost:3306/hogeTest"
test.db.default.user="hogeTestUser"
test.db.default.password="hogeTestPassWord"

# Connection Pool settings
test.db.default.poolInitialSize=10
test.db.default.poolMaxSize=20
```

### Bパターン

db.(任意名).* で定義する方法

```scala:conf/application.conf（Bパターン）
# テスト用サンプルB
# Database configuration
db.test.driver="com.mysql.jdbc.Driver"
db.test.url="jdbc:mysql://localhost:3306/hogeTest"
db.test.user="hogeTestUser"
db.test.password="hogeTestPassWord"

# Connection Pool settings
db.test.poolInitialSize=10
db.test.poolMaxSize=20
db.test.poolConnectionTimeoutMillis=1000

```

## 切り替え方

### Aパターン

Aパターンの場合は、コンフィグの初期化で切り替えます。
2章で記載した「scalikejdbc.config.DBs.setupAll」を別のコンフィグで読み込むメソッドに置き換えます。

```scala:コンフィグ初期化
//test.db.default.*を読み込む
scalikejdbc.config.DBsWithEnv("test").setupAll
```

### Bパターン
チュートリアルどおりですが、AutoRollbackの継承で定義します。

```scala:SampleAutoRollbackWithFixture

trait SampleAutoRollbackWithFixture extends AutoRollback {
  // db.test.*を読み込む
  override def db = NamedDB('test).toDB

}
```

## 切り替え方まとめ

DBアクセス部だけのテストではパターンBでいい気もします。
ただ、running(FakeApplication())やBeforeExample、AfterExampleを使用してDBアクセスのテストをする場合には、Aパターンの方が取り回しがいい気がします。

# まとめ

僕自身はパターンAを採用しましたので、パターンAでのやり方の解法の一例をあげておきます。
多少は修正が必要なモノの、AutoGenerateはテストまで自動生成してくれるので非常に便利です。


```Scala:src/test/scala/models/SampleSpec.scala編集後（Aパターン）
package models 

import scalikejdbc.specs2.mutable.AutoRollback
import org.specs2.mutable._
import scalikejdbc._

sealed trait SampleAutoRollbackWithFixture extends AutoRollback {

  override def fixture(implicit session: DBSession) {
    SQL("insert into sample values (?, ? ,?)").bind(1, "MyString", "http://test.com").update.apply()
  }
}

class SampleSpec extends Specification {
  val s = Sample.syntax("s")

  config.DBsWithEnv("test").setupAll

  "Sample" should {
    "find by primary keys" in new SampleAutoRollbackWithFixture {
      val maybeFound = Sample.find(1L)
      maybeFound.isDefined should beTrue
    }
    "find all records" in new SampleAutoRollbackWithFixture {
      val allResults = Sample.findAll()
      allResults.size should be_>(0)
    }
    "count all records" in new SampleAutoRollbackWithFixture {
      val count = Sample.countAll()
      count should be_>(0L)
    }
    "find by where clauses" in new SampleAutoRollbackWithFixture {
      val results = Sample.findAllBy(sqls.eq(s.id, 1L))
      results.size should be_>(0)
    }
    "count by where clauses" in new SampleAutoRollbackWithFixture {
      val count = Sample.countBy(sqls.eq(s.id, 1L))
      count should be_>(0L)
    }
    "create new record" in new SampleAutoRollbackWithFixture {
      val created = Sample.create(id = 2L, name = "MyString")
      created should not beNull
    }
    "save a record" in new SampleAutoRollbackWithFixture {
      val entity = Sample.findAll().head
      val modified = entity.copy(name = "Changed")
      val updated = Sample.save(modified)
      updated should not equalTo (entity)
    }
    "destroy a record" in new SampleAutoRollbackWithFixture {
      val entity = Sample.findAll().head
      Sample.destroy(entity)
      val shouldBeNone = Sample.find(1L)
      shouldBeNone.isDefined should beFalse
    }
  }

}

```