概要
- Play Frameworkのテストコードで
play.api.Application
のインスタンスを生成したら必ずplay.api.Play.stop()に渡して終了させる必要がある。
- そうしなかった場合、コネクションプールも破棄されないので、やがてDBのコネクションが枯渇しテストが失敗するようになる。
- GuiceOneAppPerSuiteなどのtraitを正しく使うか、そうでなければ
Play.stop()
にApplication
のインスタンスを渡す必要がある。
背景
私はScala/Play Frameworkを使ったアプリケーションの開発に携わっていますが、そのアプリケーションはこれまでテストがあまり書かれてこなかったため、少しずつ健全な状態に戻していきたいと思いました。最初に手を付けたのが以下の2点でした。
- GitHubにpushした際、prを出した際にテストが実行されるようCIを整備する
- 現在失敗するテストを全て修復する
ところが、sbt test
を実行すると途中までは正常に実行されるのですが、いつも同じところでDBのコネクションが枯渇し、その後のテストが全て落ちてしまい、テストコードの大半が機能しなくなっていました。
おま環の可能性も考えましたが、試しにCIを構築してテストを回しても結果は同じでした。
Play Frameworkのコネクションプールの動作
Play Frameworkの公式ドキュメントでDBに関するconfigについて確認すると、play.db.prototype.pool
, play.db.pool
に何も指定していない場合はPlay Framework2.8ではHikariCPがデフォルトで使用されることが分かります。*1
また、HikariCPはコネクションプールが生成された瞬間play.db.hikaricp.maximumPoolSize
で指定した数だけDBとのコネクションを確立します。*2
Applicationのインスタンスが生成されると同時にコネクションプールも生成されるため、Applicationのインスタンスを作りっぱなしで放置するとsbt test実行中コネクションプールが残り続け、途中でDB側のコネクション数の上限に引っかかってしまうので新しいコネクションプールを生成できなくなり、それに伴ってApplicationのインスタンスの生成にも失敗するようになりエラーが発生するようになります。
対策
GuiceOneAppPerSuite
など用意されたtraitの枠組みに乗っかる
play.api.Play.stop()
にApplicationのインスタンスが確実に渡されるようにすれば解消します。
例として、ScalaTest PlusとGuiceを使っている場合は、GuiceOneAppPerSuite
というtraitをmixinするのが手っ取り早いです。
当然テストクラスにGuiceOneAppPerSuiteをmixinするだけで勝手にApplicationをstop()してくれるわけではなく、正しい方法でtraitのインスタンス変数やメソッドをoverrideする必要があります。
GuiceOneAppPerSuite
はBaseOneAppPerSuite
とGuiceFakeApplicationFactory
を合成したtraitになっています。
BaseOneAppPerSuite.run()
の処理を確認すると
GuiceFakeApplicationFactory.fakeApplication()
が返すインスタンスがインスタンス変数app
に格納されるapp
に入ったApplicationのインスタンスがPlay.start()
に渡される- テストが実行される
app
に入ったApplicationのインスタンスがPlay.stop()
に渡される
という流れになっています。
つまり、GuiceOneAppPerSuite
を正しく機能させるには以下のどちらかが必要になります。
fakeApplication()
をoverrideしてテストに実際に使いたいApplicationのインスタンスを返すよう実装するapp
をoverrideしてfakeApplication()
から取得したインスタンスを格納するのではなくテストに使うインスタンスを格納するよう実装する
GuiceFakeAppicationFactory
のコメントを読む限りではfakeApplication()
をoverrideすることを想定していそうなので、私はそのようにしています。
基本はGuiceOneAppPerSuite
など用意された仕組みで何とかすると良いでしょう。
テストケースごとにModuleをoverrideしたい場合
テストケースごとにDIで注入するオブジェクトを変更したい場合については、どうやらそのためのtraitは用意されておらず、try-finallyのfinallyブロックでPlay.stop()
にApplication
のインスタンスをを渡してやる他無さそうでした。
しばしば使う処理だったので、私はApplication => Unit
型のテストを行う処理を受け取るloan patternのメソッドをテスト関連のUtilityに追加しました。
def withApplication(overrideModules: GuiceableModule*)(block: Application => Unit): Unit = { val app = new GuiceApplicationBuilder().overrides(overrideModules: _*).build() Play.start(app) try { block(app) } finally { Play.stop(app) } }