この記事は Play framework 2.x Scala Advent Calendar 2013 の20日目の記事です。
6日ぶり2回目…
playframeworkを使う上で、こんな感じでテスト書いてます的な
突然お昼に振られたので、書いてますww
前回同様これからPlayframeworkを触りだすお隣チーム向けでしょうか?
Controllerのテストをする
- 目的はWithApplication/WithServerから出来る限り逃げる
playのControllerをUnit Testするサンプルです
テスト系のドキュメントは2.1系から急に充実した感ありますので、2.0系以来その辺見てない〜という方は見るだけで幸せ(?)になれるかもしれません
http://www.playframework.com/documentation/2.2.x/ScalaTest ← ここの一番下のUnit Testing Controllers ですね
http://www.playframework.com/documentation/2.2.x/ScalaFunctionalTest ← 実際にはこっちの下の方の Testing a controllerかな?
上記のように書くことでControllerのメソッドをテストする際にWithApplication/WithServerからだいたい逃げられます ← すごく重要
なお、分離しても play.api.Play.current
が必要〜みたいな状況にしてしまうとダメです
実行時にRuntimeException: There is no started application
とか言われるときは、だいたいそれです
なんとか分離しましょう
ということで下に一応サンプル
なんかこう色々と突っ込みどころありますけど気にしないで
trait SampleController {
this: Controller =>
def hoge(x:Int) = Action.async { implicit request =>
Future{
val y = Form( "multi" -> optional(number)).bindFromRequest.fold(
error => 0,
optM => optM.getOrElse(2)
)
x * y
}.map(res => Ok(Json.obj("res" -> res)))
}
}
import org.specs2._
import play.api.test._
import play.api.test.Helpers._
import play.api.libs.json._
import play.api.mvc.Controller
class SampleControllerSpec extends Specification {
def is = s2"""
### path parameterに2を渡す
optionパラメータなしなら ${E1ExpectVal.toString} を返す $e1
optionパラメータ10なら ${E2ExpectVal.toString} を返す $e2
optionパラメータに文字列なら ${E3ExpectVal.toString} を返す $e3
"""
val baseExpectResult:JsValue = {
val s =
s"""
|{
| "res" : 4
|}
""".stripMargin
Json.parse(s)
}
val expectResultTransform = { res:Int => __.json.update( (__ \ 'res).json.put(JsNumber(res))) }
val E1ExpectVal = 4
val E2ExpectVal = 20
val E3ExpectVal = 0
// 以下全て成功する前提にしている
val e1Expect = baseExpectResult.transform(expectResultTransform(E1ExpectVal)).get
val e2Expect = baseExpectResult.transform(expectResultTransform(E2ExpectVal)).get
val e3Expect = baseExpectResult.transform(expectResultTransform(E3ExpectVal)).get
val target = {
val c = new SampleController with Controller
c.hoge(2)
}
def e1 = {
val res = target(FakeRequest())
(status(res) === OK).updateMessage(s => s"s : statusがおかしい").orThrow
contentAsJson(res) === e1Expect
}
def e2 = {
val res = target(FakeRequest(GET, "/?multi=10"))
(status(res) === OK).updateMessage(s => s"s : statusがおかしい").orThrow
contentAsJson(res) === e2Expect
}
def e3 = {
val res = target(FakeRequest(GET, "/?multi=hoge"))
(status(res) === OK).updateMessage(s => s"s : statusがおかしい").orThrow
contentAsJson(res) === e3Expect
}
}
長いけど全部載せました
もちろん普段はもっとちゃんと書いてますw
単にControllerのメソッドをUnitテストしたいのなら
実はtraitにする必要性は全くありません
Action#applyをみると、引数にRequestを受け取っていることがわかります
なので、
def hoge = Action { request => Ok}
をとりあえず動かしたかったら、
hoge(FakeRequest())
で動きます、いやった〜
POSTリクエストでJsonをdataとして渡したいとか、色々とあると思いますが、
FakeRequestをちら見すればとりあえずだいたい解決します
じゃあなんでtraitにするの?
object SampleController extends Controller
でいいじゃんという話ですけど、
例えば永続化層へのつなぎ込みとかが必要になった場合にtraitのほうが都合が良いです
→ 最初に上げた、play.api.Play.current
から逃げる方法にもなります
その他、所詮traitなんで、上手に分離すると色々と使い回せる?
// routeからはこっちを参照
object Application extends Controller with SampleController {
val hogeModel = HogeModel
}
// ロジックは分離
trait SampleController {
this : Controller =>
def hogeModel : HogeModel
def hoge() = Aciton{request =>
hogeModel.find(...
// 略
}
class SampleControllerSpec ... {
// test時はmockに差し替える
val targetController = new SampleController extends Controller {
def hogeModel = MockHogeModel
}
// 以下なんかテスト
}
cake pattern 使うなら使うでも良いと思いますが、この辺は割りとテキトーにやっています
個人的には上記の様な例だと、testはprojectを分けたくなります
分けることによってMock/StubをTestプロジェクトで使いまわしやすいと思うからです
階層構造的にはこのような感じでしょうか?
.
├── build.sbt
└── modules
├── web
│ ├── app
│ │ ├── controllers
│ │ │ └── SampleController.scala
│ │ └── models
│ │ └── HogeModel.scala
│ └── conf
└── webtest
├── app
│ └── models
│ └── MockHogeModel.scala
└── test
├── controllers
│ └── SampleControllerSpec.scala
└── models
└── HogeModelSpec.scala
webtest.dependson(web) 的な
デメリットは言わずもがな、最初の構築がめんどい(なれればそうでもないんですけど…)
あとフツーにMockitoとか使えばいいじゃんとかあると思いますが、
その辺はきっと好み?
個人的には上記構造だと webtest モジュールにMock的なものだったりFixture的なものだったりするものはゴリゴリ切り出せるので、ガリガリ切り出しています
PlayでUnitTest とは関係なくなってきている気がする…
FakeApplicationとかはいつ使っているのか?
あくまで個人的にはですが、基本的にはrouteまで確認するような結合テスト的な、シナリオテスト的なところで使えばいいんじゃないかな…みたいな。
まあ面倒なので出来る限り使わないようにしています(おい)
まとめ
そもそもフツーのsbtのプロジェクトとして切り出し、playはroutingとcontrollerだけ使うようにするのがいい気がする
という状況が本当は一番テストしやすいと思いますw
明日は… まだ誰もいない?
気が向いたらこの辺でやっていることを別エントリーで書きます(たぶん