概要
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層
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層
import scala.concurrent.Future
trait MyService {
def exec(str: String): Future[String]
}
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層
trait MyDao {
def exec(): Option[String]
}
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という仕組みを使います。
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のテスト
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のテスト
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のテスト
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、どんどん使っていきましょう。