始めに
普段APIばっかりなんで「AkkHttp」で足りてたけど、「Play Framework」だってかまえないと駄目だろう!
ってことで「Play」+「slick」に手をつけてみたんですが、「Scalatest」でのリポジトリの単体テストに困ったのでメモ代わりに書いておく。
環境
- Play 2.6
- play-slick 3.0.3
- play-slick-evolutions 3.0.3
ソース
データベース定義
PlayのEvolutionsを利用してMySQL上に以下のようなリレーショナルなデータベースを定義することを想定しています。
実装具合
以下のように実装してあります。
※import等は省略
モデル/テーブル
object Model {
// グループモデル
final case class Group(id: Option[Int], name: String)
// ユーザーモデル
final case class User(id: Option[Int], name: String, email: String, groupId: Int)
}
trait TableSchema {
this: HasDatabaseConfigProvider[JdbcProfile] =>
import profile.api._
// グループテーブルアクセサ
protected val groups = TableQuery[GroupTable]
// ユーザーテーブルアクセサ
protected val users = TableQuery[UserTable]
// グループテーブル
protected class GroupTable(tag: Tag) extends Table[Group](tag, "group") {
def id = column[Int]("id", O.PrimaryKey, O.AutoInc)
def name = column[String]("name")
override def * = (id.?, name) <> (Group.tupled, Group.unapply)
}
// ユーザーテーブル
protected class UserTable(tag: Tag) extends Table[User](tag, "user") {
def id = column[Int]("id", O.PrimaryKey, O.AutoInc)
def name = column[String]("name")
def email = column[String]("email")
def groupId = column[Int]("group_id")
def group =
foreignKey("FK_user_TO_group", groupId, groups)(_.id,
onUpdate = ForeignKeyAction.Restrict,
onDelete = ForeignKeyAction.Restrict)
override def * = (id.?, name, email, groupId) <> (User.tupled, User.unapply)
}
}
リポジトリ
// グループリポジトリ
@Singleton
class GroupRepository @Inject()(override protected val dbConfigProvider: DatabaseConfigProvider)
extends TableSchema
with HasDatabaseConfigProvider[JdbcProfile] {
import profile.api._
//** 単純なCRUDなので省略 **/
}
// ユーザーリポジトリ
@Singleton
class UserRepository @Inject()(override protected val dbConfigProvider: DatabaseConfigProvider)
extends TableSchema
with HasDatabaseConfigProvider[JdbcProfile] {
import profile.api._
//** 単純なCRUDなので省略 **/
}
ここから困ったこと
-
ScalaTest
+Guice
でのテスト方法がよく分からん。公式のサンプルはSpec2
使ってる。 - リレーション組んであるテーブルの子であるリポジトリをテストする際に、親側に事前にデータを用意したいんだけどどうすんの?
何とかする
1について
この辺を使えば何とかなる気がする。
2について
要はリポジトリの外でテーブルにアクセスすればどうにかできそう。slickのコネクションを別に起てて、SQL直書きという手も考えたけど、スキーマが変わったときに面倒なので却下。
極力TableSchema
を使いたいが、自分型HasDatabaseConfigProvider[JdbcProfile]
を持つので継承先でprotected val dbConfigProvider: DatabaseConfigProvider
をオーバーライドする必要が・・・。
DatabaseConfigProvider
は本来Guice
が自動生成するはず。
結論
1・2を踏まえて以下のようなtrait
を作ってみました。
package com.example.repository
import scala.concurrent.ExecutionContext
import akka.Done
import org.scalatest.BeforeAndAfterAll
import org.scalatest.concurrent.ScalaFutures
import org.scalatest.time.{Millis, Seconds, Span}
import org.scalatestplus.play.PlaySpec
import org.scalatestplus.play.guice.GuiceOneAppPerSuite
import play.api.Application
import play.api.db.slick.{DatabaseConfigProvider, HasDatabaseConfigProvider}
import play.api.inject.guice.GuiceApplicationBuilder
import slick.jdbc.JdbcProfile
// MySQLバージョンってことで
trait MySQLRepositorySpec
extends PlaySpec
with GuiceOneAppPerSuite
with ScalaFutures
with BeforeAndAfterAll
with TableSchema
with HasDatabaseConfigProvider[JdbcProfile] {
implicit lazy val executor: ExecutionContext = fakeApplication.actorSystem.dispatcher
override implicit val patienceConfig: PatienceConfig =
PatienceConfig(timeout = Span(10, Seconds), interval = Span(1000, Millis))
// TableSchemaの継承条件を満たす
protected override lazy val dbConfigProvider: DatabaseConfigProvider =
fakeApplication.injector.instanceOf[DatabaseConfigProvider]
// PlayApplicationの生成
override lazy val fakeApplication: Application =
new GuiceApplicationBuilder()
.configure(Map(
"slick.dbs.default.profile" -> "slick.jdbc.MySQLProfile$",
"slick.dbs.default.db.driver" -> "com.mysql.jdbc.Driver",
"slick.dbs.default.db.url" -> "jdbc:mysql://127.0.0.1:3306/test?useSSL=false&",
"slick.dbs.default.db.user" -> "root",
"slick.dbs.default.db.password" -> "1234"
))
.build()
// この辺は適当
override def afterAll(): Unit = {
val _ = fakeApplication.stop().mapTo[Done].futureValue
()
}
}
GuiceOneAppPerSuite
のfakeApplication
にslickの設定を定義し、利用しています。build.sbt
にplay-slick-evolutions
を追加していれば自動でEvolutionsが起動するはずです。
このtrait
を以下のように使います。
package com.example.repository
import com.example.core.Model.{Group, User}
class UserRepositorySpec extends MySQLRepositorySpec {
import profile.api._
var existGroupId = 0
private val repository = fakeApplication.injector.instanceOf[UserRepository]
override def beforeAll(): Unit = {
// groupテーブルの事前定義
existGroupId =
db.run((groups returning groups.map(_.id)) += Group(None, "test-group")).futureValue
()
}
override def afterAll(): Unit = {
val _ = db.run(users.delete andThen groups.delete).futureValue
super.afterAll()
}
// UserRepositoryのスコープは保たれる
"user repository" should {
"create" in {
val createUser = User(None, "test-user", "test@test.com", existGroupId)
val createdId = repository.create(createUser).futureValue
repository.findById(createdId).futureValue match {
case Some(createdUser) => createdUser mustBe createUser.copy(id = Option(createdId))
case None => fail("not found user")
}
}
}
}
TableSchema
へのアクセスをテスト本体部分に置いちゃうと混乱するかも・・・。
終わりに
う~ん・・・こんなやり方であってるんだろうか?