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

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

## 環境
- Play 2.6
- play-slick 3.0.3
- play-slick-evolutions 3.0.3

## ソース
https://github.com/lightstaff/play-slick-test-example

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

![group-user.jpg](https://qiita-image-store.s3.amazonaws.com/0/94252/be8c5c13-4773-e42e-3241-7ac94fc7827c.jpeg)

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

## モデル/テーブル

```scala: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)
}
```

```scala: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)
  }
}

```

## リポジトリ

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

  import profile.api._

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

```scala: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`でのテスト方法がよく分からん。[公式のサンプル](https://github.com/playframework/play-scala-slick-example)は`Spec2`使ってる。
2. リレーション組んであるテーブルの子であるリポジトリをテストする際に、親側に事前にデータを用意したいんだけどどうすんの？

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

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

```scala: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
    ()
  }
}
```

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

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

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


# 終わりに
う～ん・・・こんなやり方であってるんだろうか？