LoginSignup
7
8

More than 5 years have passed since last update.

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

Last updated at Posted at 2016-08-12

概要

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

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

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

環境

  • Play 2.5
  • scalaTest

ソースコードはこちら。

DIの準備

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

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

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

Controller層

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層

MyService.scala
import scala.concurrent.Future

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

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層

MyDao.scala
trait MyDao {
  def exec(): Option[String]
}
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という仕組みを使います。

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のテスト

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のテスト

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のテスト

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、どんどん使っていきましょう。

7
8
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
8