0.前座
ScalikeJDBCを使ってのMySQL操作で遊んでみました。
Play Frameworkのプラグインもあり、導入コストもかなり低く重宝しています。
導入の仕方についての記事は、他の方々がよりよい感じで書いてくれてるとは思うので割愛。
プロダクトコードだと、テストでは本番のDBにつないでほしくない!
そういった方のための記事です。
設定はこんな感じです。
//前略
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
//mysql
libraryDependencies += "mysql" % "mysql-connector-java" % "5.1.33"
//DBコード自動生成
addSbtPlugin("org.scalikejdbc" %% "scalikejdbc-mapper-generator" % "2.2.0")
10000:scalikejdbc.PlayPlugin
11000:scalikejdbc.PlayFixturePlugin
# 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は非常に楽で、コマンド一発でテスト書いてくれます。
$ #play Framework 2.3で試したので、SBT使う人はsbtと読み替えてくださいね)
$ ./activator "scalikejdbc-gen [TableName]"
が、このままだとTest - Failもしくはerrorしてしまいます。
なぜ動かないのかを説明こみで、動くテストをつくっていきましょう。
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つに書くほうがいいとかんがえますが、
何度も書くとめんどくさいのでコンストラクタで書いてしまえばいいと思います。
scalikejdbc.config.DBs.setupAll //コンフィグのセットアップ
//Append DB切り替えでコンフィグが決まってる場合にはこちらでも。
//scalikejdbc.config.DBs.setup('test) //db.test.* の読み込みのみの場合はこちら
アップデートがアップデートしていないテストに!
よくよくみるとアップデートしてないやーん!というテストになってます。
"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)
}
なので、アップデートさせましょう
"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」をオーバーライドします。
trait SampleAutoRollbackWithFixture extends AutoRollback {
override def fixture = {
//ダミー用のDB作成コードを書く
SQL("insert into sample values (?, ? ,?)").bind(1, "MyString", "http://test.com").update.apply()
}
}
そして、今まで new AutoRollbackとしてたところを新しく作ったTraitに変更します。
//"find by primary keys" in new AutoRollback {
"find by primary keys" in new SampleAutoRollbackWithFixture {
//省略
主キーが重複
上記で作成した自前のAutoRollbackを使用すると主キー重複でテストがこけてしまうので、追加するレコードと異なる主キーの値を設定します。
(このテストのみ、自前のAutoRollbackを使用しないのも手です)
"create new record" in new SampleAutoRollbackWithFixture {
val created = Sample.create(id = 1L, name = "MyString")
created should not beNull
}
"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.* で定義する方法
# テスト用サンプル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.(任意名).* で定義する方法
# テスト用サンプル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」を別のコンフィグで読み込むメソッドに置き換えます。
//test.db.default.*を読み込む
scalikejdbc.config.DBsWithEnv("test").setupAll
Bパターン
チュートリアルどおりですが、AutoRollbackの継承で定義します。
trait SampleAutoRollbackWithFixture extends AutoRollback {
// db.test.*を読み込む
override def db = NamedDB('test).toDB
}
切り替え方まとめ
DBアクセス部だけのテストではパターンBでいい気もします。
ただ、running(FakeApplication())やBeforeExample、AfterExampleを使用してDBアクセスのテストをする場合には、Aパターンの方が取り回しがいい気がします。
まとめ
僕自身はパターンAを採用しましたので、パターンAでのやり方の解法の一例をあげておきます。
多少は修正が必要なモノの、AutoGenerateはテストまで自動生成してくれるので非常に便利です。
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
}
}
}