28
27

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

PlayFramework2.4におけるDIのテスト方法

Posted at

 ***PlayFramework2.4***では Guice を使って正式に DI 機能が導入されました。DIを導入するメリットの一つとしてテストにおけるモックが凄くシンプルに実装できる点があります。

今回は簡単なユーザー認証の例を通じて、自分の試行錯誤中に勉強した、PlayFramework2.4におけるDIのテスト方法を紹介したいと思います。

DI機能を使っての実装

■ まず、データを取得するためのRepositoryのインタフェースの定義及び実装を行います。

※ テストで使わないので、実装は省略します。

case class User(username: String, password: String)

trait UserRepository {
  def resolve(username: String): Option[User]
}

class UserRepositoryOnJDBC extends UserRepository {
  def resolve(username: String) : Option[User] = {
    ...
  }
}

■ 次は、DI機能を使ってRepositoryを利用するServiceを作成します。(今回のテスト対象です)

※ 説明のため、セキュリティなどの問題は無視してください。

class AuthService @Inject()(userRepository: UserRepository) {
  def auth(user: User): Boolean = {
    userRepository.resolve(user.username) match {
      case Some(u) => u == user
      case None => false
    }
  }
}

■ DI機能を使ってServiceを利用するControllerを作成します。(今回のテスト対象です)

class AuthController @Inject()(val authService: AuthService) extends Controller {
  val loginForm = Form(
    mapping(
      "username" -> nonEmptyText,
      "password" -> nonEmptyText
    )(User.apply)(User.unapply)
  )

  def login = Action { implicit request =>
    loginForm.bindFromRequest.fold(
      _ => BadRequest(),
      user => if(authService.auth(user)) {
        Ok()
      } else {
        Unauthorized()
      }
    )
  }
}

■ テストのためRouteを定義します。

POST        /login               controllers.AuthController.login

■ 最後にModuleでRepositoryの実装クラスを指定します。

※ AuthServiceはインタフェースではないので、自動Injectできます。

class GuiceModule extends AbstractModule {
  def configure() = {
    bind(classOf[UserRepository]).to(classOf[UserRepositoryOnJDBC])
  }
}

DI機能のテスト方法

ここからは***specs2*** を使って、上記の例のテストのしかたを紹介します。
では、実際のテストコードを見てみましょう。

■ Serviceのテスト

まず、一番シングルの例としてUserRepositoryをモック化、UserServiceをテストします。

class UserServiceSpec extends Specification with Mockito {
  "UserService#auth" should {
    "ユーザー名、パスワードが正しいの場合、Trueを返す" in {
      // setup
      val testUser = User("user1", "password1")
      val userRepository = mock[UserRepository]
      userRepository.resolve("user1") returns testUser
      val userService = new UserService(userRepository)
      // execute & verify
      userService.auth(User("user1", "password1")) must beTrue
    }
  }
  ...
}

■ Controllerのテスト

Controllerをテストする際には、リクエストする必要があります。PlayFrameworkは幾つのツールを提供しています。

  • FakeRequest, call()

これらを使えば、直接Actionをテストすることができます。Controllerの単体テストに使えます。

class AuthControllerSpec extends Specification with Mockito {
  "AuthController" should {
    "ユーザー名、パスワードが正しいの場合、ログインできます" in {
      // setup
      val request = FakeRequest(POST, "/login").withFormUrlEncodedBody(
        "username" -> "user1",
        "password" -> "password"
      )
      val userService = mock[UserService]
      userService.auth(any) returns true
      val controller = new AuthController(userService)
      // execute
      val actual = call(controller.authenticate(),request)
      // verify
      status(actual) must equalTo(OK)
    }
  }
}
  • WithApplication, FakeApplication, route()

こちらを使えば、Actionを呼び出す代わりにRouterからテストできます。機能テストに使えます。
他にも WithServer, WithBrowser, PlaySpecification などのツールがありますが、DIのテストに特に関係ないので省略します。

※ 興味のある方は Writing functional tests with specs2 でご確認ください。

■ Guiceを使っての機能テスト

PlayFramework2.4ではGuiceを使ってDIを実装したアプリケーションに対して、GuiceApplicationBuilderGuiceInjectorBuilder の2つのBuilderを提供しています。テスト中にこの2つのクラスを使って、アプリケーションの環境、配置、或いは依存関係を直接設定できます。

※ 詳しい説明は Testing With Guice でご確認ください。

class MockUserRepository extends UserRepository {
  def resolve(username: String) : Option[User] = Some(
      User("user1", "password")
    )
}

class AuthControllerSpec extends Specification {
  val app: Application = new GuiceApplicationBuilder()
    .overrides(bind[UserRepository].to[MockUserRepository])
    .build()

  "AuthController" should {
    "ユーザー名、パスワードが正しいの場合、ログインできます" in new WithApplication(app) {
      val request = FakeRequest(POST, "/login").withFormUrlEncodedBody(
        "username" -> "user1",
        "password" -> "password"
      )
      val result = route(request).get
      status(result) must equalTo(OK)
    }
  }
}

まとめ

すごく基本の話ですが、以上となります。テストは成功のケースだけを書きましたが、他のケースも同じ書き方で簡単にできると思います。

DI機能を導入することで、PlayFramework2.4がモジュール間の依存関係をなくし、単体テストも機能テストも簡単かつシンプルにできるようになりましたね。
今やっているプロジェクトもPlay2.4をWebServiceポートとして使われて、複雑のロジックは別のレイヤーで隔離させています。Play2.4でロジックを簡単にモック化できるので楽です。

最後まで読んでいただきありがとうございました。


本記事はセプテーニオリジナル開発者ブログの転載です。
http://labs.septeni.co.jp/entry/2015/12/10/115611

28
27
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
28
27

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?