Play2のGuice DIを使う＆テストも書く


## 概要

Playは、以前はDI要らないとか言っていたような気がしましたが、2.3~2.5あたりからGlobal依存を辞めてDIベースの実装になるようにどんどん改良されています。

ここで改めて、Play（というかWebアプリ全般）でDIを使うとどのように嬉しいのかを試してみたいと思います。

（なお、Scalaでは静的DIと呼ばれるものが主流な気もしますが、ここではGuiceによる動的なDIを対象とします。）

## 環境

- Play 2.5
- scalaTest

ソースコードはこちら。

https://github.com/uryyyyyyy/play2sample/tree/guiceDI

## DIの準備

Java界隈では割と有名な責務分割として、

- Controller層
  - MVCのC。
  - リクエストを受け付けてServiceに処理させてViewに投げる役割を担う。
  - 結果に応じてレスポンスを変えたりするが、基本ロジックを持たない。
- Service
  - メインの処理を担う。
  - 外部接続系は後述のDAOのAPIを呼び出す。
- DAO
  - 外部リソースのアクセスを担う。

という感じで分けられますね。
これを参考にします。

### Controller層

```scala:MyDIController.scala
import javax.inject.{Inject, Singleton}

import akka.actor.ActorSystem
import com.github.uryyyyyyy.services.MyService
import play.api.mvc.{Action, Controller}

import scala.concurrent.ExecutionContext

@Singleton
class MyDIController @Inject() (myService: MyService, actorSystem: ActorSystem) extends Controller {

  implicit val myExecutionContext: ExecutionContext = actorSystem.dispatcher

  def message = Action.async {
    myService.exec("str").map { msg => Ok(msg) }
  }
}
```

ここではServiceをDIしています。（actorSystemはFutureのために仕方なく）
特に必要ないのでSingletonです。

### Service層

```scala:MyService.scala
import scala.concurrent.Future

trait MyService {
  def exec(str: String): Future[String]
}

```

```scala:MyServiceImpl.scala
import javax.inject.{Inject, Singleton}

import akka.actor.ActorSystem
import com.github.uryyyyyyy.daos.MyDao

import scala.concurrent.{ExecutionContext, Future}

@Singleton
class MyServiceImpl @Inject() (myDao: MyDao, actorSystem: ActorSystem) extends MyService {

  implicit val myExecutionContext: ExecutionContext = actorSystem.dispatcher

  def exec(str: String): Future[String] = {
    Future{
      str + " " + myDao.exec().getOrElse("null")
    }
  }
}

```

myServiceはあくまでtraitで、実装はImplの方に書きます。こうすることで、Controllerが実装に依存しなくなります。
ここではmyDaoをDIしていますね。こちらもSingletonにします。
（今回は簡単にするため省略していますが、RDB接続でトランザクションを使う場合は、一般的にService層で管理をすることになると思います。）

### DAO層

```scala:MyDao.scala
trait MyDao {
  def exec(): Option[String]
}
```

```scala:MyDaoImpl.scala
import javax.inject.{Inject, Singleton}

import play.api.db.{Database, NamedDatabase}
import scalikejdbc._

@Singleton
class MyDaoImpl @Inject() (@NamedDatabase("mySample") db: Database) extends MyDao {

  def exec(): Option[String] = {
    using(db.getConnection(autocommit = false)) { conn =>
      val ss = DB(conn).readOnly { implicit session =>
        sql"select 2".map( rs => rs.long(1)).single.apply()
      }
      ss.map(_.toString)
    }
  }
}
```

ここではMyDaoの実装としてImplを用意しています。
依存するものはDBのコネクションプールです。これによって、テスト時にコネクションを差し替えることが容易になります。
（DBはapplication.confにmySampleという名前で設定が書いてあることとします。）

## Play起動時に動的DIを行う

さて、ここまででそれぞれ抽象・具象を作り、抽象のみに依存する形が作れました。
しかし、これだけではどの具象を使えばいいかわからないので、Guiceを用いで実行時にDIしていきます。

そのために、PlayではModuleという仕組みを使います。

```scala:ImplModule.scala
import com.github.uryyyyyyy.daos.{MyDao, MyDaoImpl}
import com.github.uryyyyyyy.services.{MyService, MyServiceImpl}
import com.google.inject.AbstractModule

class ImplModule extends AbstractModule {

  override def configure() = {
    bind(classOf[MyService]).to(classOf[MyServiceImpl])
    bind(classOf[MyDao]).to(classOf[MyDaoImpl])
  }
}
```

これをapplication.confで起動時に読むように設定します。

```
play.modules {
  enabled += modules.ImplModule
  #enabled += modules.MockModule

  # If there are any built-in modules that you want to disable, you can list them here.
  #disabled += ""
}
```

今回はImplModuleを使いますが、別のModuleを呼びだせば別の実装を使うことが出来るようになります。
大規模開発や開発時のモックサーバ用途に使えるかもしれません。

（ちなみに、ここをミスると起動時にわりとわかりにくいエラーが出るので、慣れてないとハマるかもです。）

さて、ここまでの設定ができていれば、アクセス時に`mySample`のDB設定を用いてSQLを発行し、その結果を返してくれるはずです。

## テストを書く

さて、上記の実装は慣れないとややこしいですね。
なぜこのようなことをするかというと、粗結合にしてテストしやすくするためです。

### Controllerのテスト

```scala:MyDIControllerTest.scala
import java.util.concurrent.TimeUnit

import akka.actor.ActorSystem
import akka.util.Timeout
import com.github.uryyyyyyy.services.MyService
import org.mockito.Matchers.any
import org.mockito.Mockito._
import org.scalatest.mock.MockitoSugar
import org.scalatest.{FunSpec, MustMatchers}
import play.api.mvc.Result
import play.api.test.{FakeRequest, Helpers}

import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future

class MyDIControllerTest extends FunSpec with MustMatchers with MockitoSugar  {

  describe("MyDIControllerTest") {

    implicit val timeout = Timeout(5000, TimeUnit.MILLISECONDS)

    it("controllerTest"){
      val mockService = mock[MyService]
      when(mockService.exec(any[String])) thenReturn Future{"str"}

      val actorSystem = ActorSystem.apply()
      val controller = new MyDIController(mockService, actorSystem)

      val result: Future[Result] = controller.message().apply(FakeRequest())
      Helpers.contentAsString(result) mustBe "str"
      Helpers.status(result) mustBe 200
    }
  }

}
```

ここでは、Controllerが依存しているSerivceをMockでDIして、Controllerのみの挙動を確認出来るように作っています。

ここでは、Serviceが返した値を表示するだけなので、contextとstatusCodeを確認しています。
場合によっては、Serviceが異常系を返したら4XXエラーを返すように書くことも容易です。


### Serviceのテスト

```scala:MyServiceImplTest.scala
import akka.actor.ActorSystem
import com.github.uryyyyyyy.daos.MyDao
import org.mockito.Mockito._
import org.scalatest.mock.MockitoSugar
import org.scalatest.{FunSpec, MustMatchers}

import scala.concurrent.Await
import scala.concurrent.duration.Duration

class MyServiceImplTest extends FunSpec with MustMatchers with MockitoSugar {

  describe("MyServiceImplTest") {

    it("service"){
      val mockDao = mock[MyDao]
      when(mockDao.exec()) thenReturn Some("mm")

      val actorSystem = ActorSystem.apply()
      val service = new MyServiceImpl(mockDao, actorSystem)
      val result = Await.result(service.exec("aaa"), Duration.Inf)
      result mustBe "aaa mm"
    }
  }

}

```

Controllerのテストと同様に、Daoをモックして動作確認をしています。
見たまんまのシンプルなテストですね。

### Daoのテスト

```scala:MyDaoImplTest.scala
import org.scalatest.mock.MockitoSugar
import org.scalatest.{FunSpec, MustMatchers}
import play.api.db.Databases

class MyDaoImplTest extends FunSpec with MustMatchers with MockitoSugar {

  describe("MyDaoImplTest") {

    it("dao"){
      val database = Databases(
        "org.h2.Driver",
        "jdbc:h2:mem:play"
      )

      val dao = new MyDaoImpl(database)
      val result = dao.exec()
      result mustBe Some("2")

      database.shutdown()
    }
  }
}
```

DAOのテストは、外部リソースにアクセスしないとできないですが、本番や開発で使ってるリソースに繋ぐわけにもいかないので、ここもDIで対象を差し替えるやり方が効いてきます。
ここではDBコネクションプールを差し替えることで安全にテストを行えるようにしています。

## まとめ

DIを使うことで、グローバルな何かに依存することがなくなり、テストがとても簡単にかけることがわかったと思います。
Guice DI、どんどん使っていきましょう。