Playのコンパイル時DIで用意するBuiltInComponetnsのクラス階層について整理します。
基礎
まずはconfに指定したApplicationLoader実装クラス(図ではMyApplicationLoader
)のloadメソッドから始まります。このメソッドの中でBuiltInComponents実装クラス(図ではApplicationComponentsFromContext
)を使ってApplicationインスタンスを生成します。
import play.api.Application
import play.api.ApplicationLoader
import play.api.ApplicationLoader.Context
class MyApplicationLoader extends ApplicationLoader {
override def load(context: Context): Application = {
new ApplicationComponentsFromContext(context).application
}
}
ApplicationComponentsFromContext
はBuiltInComponents
を実装します。BuiltInComponents
の抽象メンバのうち、Context
から導出できるものを定義したヘルパークラスがBuiltInComponentsFromContext
です。
import com.github.mumoshu.play2.memcached.api.MemcachedComponents
import play.api.ApplicationLoader.Context
import play.api.BuiltInComponentsFromContext
class ApplicationComponentsFromContext(context: Context)
extends BuiltInComponentsFromContext(context: Context)
with ApplicationComponents
with MemcachedComponents
メインとなるコンポーネント時DIはApplicationComponents
が担います。
trait ApplicationComponents
extends BuiltInComponents
with ... /* 他たくさん */ {
// playはルーターインスタンスを作りさえすれば動く。SIRDを使えば、routesファイルやコントローラーすらなくてもよい。
override lazy val router: Router = ...
}
テストを考慮した設計
例えば、本番環境ではmemcachedを使い、単体テスト実行時はcaffeineを使う場合、これらのコンポーネントを一緒に使うことはできません。コンポーネントはケーキパターンを想定しているため、同じキャッシュAPIの異なる実装を混ぜることができないのです。
trait ApplicationComponents
extends BuiltInComponents
with MemcachedComponents
with CaffeineCacheComponents
... {
// この場合 defaultCacheApi を使うと、2つのうち、どれを使えばいいかが決定できない。
// コンポーネントの実装メンバはlazy valなので、superも使えない。
}
そこで、これらを本番とテストで切り替えられるようにクラス構成を考えます。答えとしては既に図で示した通りで、本番ではApplicationComponentsFromContext
にてmemcachedを実装し、テストではFakeApplicationComponents
にてcaffeineを実装します。FakeApplicationComponents
は本番でも利用するApplicationComponents
を継承しているため、キャッシュAPI以外のワイヤリングを再利用できます。やったね。
class FakeApplicationComponents(context: Context)
extends BuiltInComponentsFromContext(context: Context)
with ApplicationComponents
with CaffeineCacheComponents
with ... {
...
}
ちなみに、このFakeApplicationComponents
はこんな感じで使います。
trait OneMyAppPerSuite extends OneAppPerSuiteWithComponents {
self: TestSuite =>
// デフォルトのフェイクコンポーネントを返す
override def components: ApplicationComponents = {
new FakeApplicationComponents(context)
}
}
class ClientControllerSpec extends AnyFunSpec with OneMyAppPerSuite {
describe("/hello") {
it("コンテンツに「hello」が含まれること") {
// ここでアプリが起動している
val path = "/hello"
val res = route(app, FakeRequest(GET, path)).get
val contents = contentAsString(res)
assert(contents.contains("hello"))
}
}
}