FLINTERS Engineer's Blog

FLINTERSのエンジニアによる技術ブログ

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

ベース記事はこちら(最新版もこちらにございます。)

http://qiita.com/yharada/items/b238750d21c9fb63928e

0.前座

ScalikeJDBCを使ってのMySQL操作で遊んでみました。

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

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

設定はこんな感じです。

build.sbt

//前略

scalaVersion := "2.11.1"

//中略

libraryDependencies ++= Seq(
  "org.scalikejdbc" %% "scalikejdbc" % "2.1.4",
  "org.scalikejdbc" %% "scalikejdbc-config" % "2.1.4",
  "org.scalikejdbc" %% "scalikejdbc-interpolation" % "2.1.4",
  "org.scalikejdbc"      %% "scalikejdbc-test"                % "2.1.4"  % "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",
  //その他ライブラリは省略
)

plugins.sbt

//mysql
libraryDependencies +=   "mysql"   %  "mysql-connector-java"   % "5.1.33"

//DBコード自動生成
addSbtPlugin("com.github.seratch" %% "scalikejdbc-mapper-generator" % "[1.6,)")

project/play.plugins

10000:scalikejdbc.PlayPlugin
11000:scalikejdbc.PlayFixturePlugin

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は非常に楽で、コマンド一発でテスト書いてくれます。

$ #play Framework 2.3で試したので、SBT使う人はsbtと読み替えてくださいね)
$ ./activator "scalikejdbc-gen [TableName]"

が、このままだとTest - Failもしくはerrorしてしまいます。
なぜ動かないのかを説明こみで、動くテストをつくっていきましょう。

SampleSpec編集前

package dbAccess.hoge

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

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

  "Sample" should {
    "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
      val updated = Sample.save(entity)
      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
    }
  }
}

importライブラリの誤り?

sqlsのインポートがオカしいっぽいので修正します。
コンパイル通らない系の単純な誤りなので、修正のみ。

修正前

import scalikejdbc.SQLInterpolation._

修正後

import scalikejdbc._

コンフィグの初期化が存在しない!

  • DBs.setupAllを実行する。

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

コンフィグ初期化

scalikejdbc.config.DBs.setupAll //コンフィグのセットアップ

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

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

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

アップデート変更前

"save a record" in new SampleAutoRollbackWithFixture {
  val entity = Sample.findAll().head
  val updated = Sample.save(entity)
  updated should not equalTo (entity)
}

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

アップデート変更後

"save a record" in new SampleAutoRollbackWithFixture {
  val entity = Sample.findAll().head
  val updated = Sample.save(entity.copy(name = "Changed"))
  updated should not equalTo (entity)
}

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

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

AutoRollBackとFixtureを使おう!

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

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

SampleAutoRollbackWithFixture

trait SampleAutoRollbackWithFixture extends AutoRollback{
   override val 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.* で定義する方法

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.(任意名).* で定義する方法

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」を別のコンフィグで読み込むメソッドに置き換えます。

コンフィグ初期化

//test.db.default.*を読み込む
scalikejdbc.config.DBsWithEnv("test").setupAll

Bパターン

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

SampleAutoRollbackWithFixture

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

切り替え方まとめ

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

まとめ

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

SampleSpec編集後(Aパターン)

package dbAccess.hoge

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
      // UPDATE内容は自分で記載が必要
      val updated = Sample.save(entity.copy(name = "Changed"))
      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
    }
  }

}