Help us understand the problem. What is going on with this article?

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

More than 3 years have passed since last update.

 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

xoyo24
格ゲーマーエンジニアです
https://xoyo24.me/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした