LoginSignup
7
3

More than 5 years have passed since last update.

Play+Slickのリポジトリの単体テストってこれでいいの?

Last updated at Posted at 2018-07-31

始めに

普段APIばっかりなんで「AkkHttp」で足りてたけど、「Play Framework」だってかまえないと駄目だろう!
ってことで「Play」+「slick」に手をつけてみたんですが、「Scalatest」でのリポジトリの単体テストに困ったのでメモ代わりに書いておく。

環境

  • Play 2.6
  • play-slick 3.0.3
  • play-slick-evolutions 3.0.3

ソース

データベース定義

PlayのEvolutionsを利用してMySQL上に以下のようなリレーショナルなデータベースを定義することを想定しています。

group-user.jpg

実装具合

以下のように実装してあります。
※import等は省略

モデル/テーブル

com.example.core.Model.scala
object Model {

  // グループモデル
  final case class Group(id: Option[Int], name: String)

  // ユーザーモデル
  final case class User(id: Option[Int], name: String, email: String, groupId: Int)
}
com.example.repository.TableSchema.scala
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)
  }
}

リポジトリ

com.example.repository.GroupRepository.scala
// グループリポジトリ
@Singleton
class GroupRepository @Inject()(override protected val dbConfigProvider: DatabaseConfigProvider)
    extends TableSchema
    with HasDatabaseConfigProvider[JdbcProfile] {

  import profile.api._

  //** 単純なCRUDなので省略 **/
}
com.example.repository.UserRepository.scala
// ユーザーリポジトリ
@Singleton
class UserRepository @Inject()(override protected val dbConfigProvider: DatabaseConfigProvider)
    extends TableSchema
    with HasDatabaseConfigProvider[JdbcProfile] {

  import profile.api._

  //** 単純なCRUDなので省略 **/
}

ここから困ったこと

  1. ScalaTest+Guiceでのテスト方法がよく分からん。公式のサンプルSpec2使ってる。
  2. リレーション組んであるテーブルの子であるリポジトリをテストする際に、親側に事前にデータを用意したいんだけどどうすんの?

何とかする

1について

この辺を使えば何とかなる気がする。

2について

要はリポジトリの外でテーブルにアクセスすればどうにかできそう。slickのコネクションを別に起てて、SQL直書きという手も考えたけど、スキーマが変わったときに面倒なので却下。
極力TableSchemaを使いたいが、自分型HasDatabaseConfigProvider[JdbcProfile]を持つので継承先でprotected val dbConfigProvider: DatabaseConfigProviderをオーバーライドする必要が・・・。
DatabaseConfigProviderは本来Guiceが自動生成するはず。

結論

1・2を踏まえて以下のようなtraitを作ってみました。

com.example.repository.MySQLRepositorySpec
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
    ()
  }
}

GuiceOneAppPerSuitefakeApplicationにslickの設定を定義し、利用しています。build.sbtplay-slick-evolutionsを追加していれば自動でEvolutionsが起動するはずです。

このtraitを以下のように使います。

com.example.repository.UserRepositorySpec
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へのアクセスをテスト本体部分に置いちゃうと混乱するかも・・・。

終わりに

う~ん・・・こんなやり方であってるんだろうか?

7
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
3