Unit testing a controller

We might have a simple project with a User model and UserRepo, defined as follows:

case class User(id: Option[Long], loginId: String, name: Option[String],
  contactNo: Option[String], dob: Option[Long], address: Option[String])
 
object User{
  implicit val userWrites = Json.writes[User]
}

trait UserRepo {
  def authenticate(loginId: String, password: String): Boolean

  def create(u: User, host: String, password: String): Option[Long]

  def update(u: User): Boolean

  def findByLogin(loginId: String): Option[User]

  def delete(userId: Long): Boolean

  def find(userId: Long): Option[User]

  def getAll: Seq[User]

  def updateStatus(userId: Long, isActive: Boolean): Int

  def updatePassword(userId: Long, password: String): Int
}

In this project, we need to test a getUser method of UserController—a controller that is defined to access user details, which are handled by the user model, where UserController is defined as follows:

object UserController extends Controller {

  /* GET a specific user's details */
  def getUser(userId: Long) = Action {
    val u = AnormUserRepo.find(userId)
    if (u.isEmpty) {
      NoContent
    }
 else {
      Ok(Json.toJson(u))
    }
  }
....
}

AnormUserRepo is an implementation of UserRepo, which uses Anorm for DB transactions. The methods in UserController are mapped in the routes file as follows:

GET        /api/user/:userId        controllers.UserController.getUser(userId:Long)

Since mocking Scala objects for tests is not yet fully supported by a testing library, there are different approaches to unit test a controller. These are as follows:

  1. Defining all the controller's methods in a trait and then this trait can be extended by an object, while the trait is tested for functionality
  2. Defining controllers as classes and wiring up other required services using dependency injection

Both these approaches require us to modify our application code. We can choose the one that suits our coding practices the best. Let's see what these changes are and how to write the corresponding tests in the following sections.

Using traits for controllers

In this approach, we define all the controller's methods in a trait and define the controller by extending this trait. For example, UserController should be defined as follows:

trait BaseUserController extends Controller {
this: Controller =>

  val userRepo:UserRepo

  /* GET a specific user's details */
  def getUser(userId: Long) = Action {
    val u = userRepo.find(userId)
    if (u.isEmpty) {
      NoContent
    } else {
      Ok(Json.toJson(u))
    }
  }

}

object UserController extends BaseUserController{
  val userRepo = AnormUserRepo
}

Now, we can write tests for the BaseUserController trait—UserControllerSpec using Specs2 as follows:

class UserControllerSpec extends Specification with Mockito {

  "UserController#getUser" should {
    "be valid" in {
      val userRepository = mock[UserRepo]
      val defaultUser = User(Some(1), "loginId", Some("name"), Some("contact_no"), Some(20L), Some("address"))
      userRepository.find(1) returns Option(defaultUser)

      class TestController extends Controller with BaseUserController{
        val userRepo = userRepository
      }

      val controller = new TestController
      val result: Future[Result] = controller.getUser(1L).apply(FakeRequest())
      val userJson: JsValue = contentAsJson(result)

      userJson should be equalTo(Json.toJson(defaultUser))
    }
  }
}

FakeRequest is a helper that generates fake HTTP requests while testing.

Here, we mock UserRepo and use this mock to generate a new instance of TestController. ScalaTest provides integration with Mockito via its MockitoSugar trait, so there will be small changes in the code for mocking.

Using ScalaTest, the UserControllerTest test will be as follows:

class UserControllerTest extends PlaySpec with Results with MockitoSugar {

  "UserController#getUser" should {
    "be valid" in {
      val userRepository = mock[UserRepo]
      val defaultUser = User(Some(1), "loginId", Some("name"), Some("contact_no"), Some(20L), Some("address"))
      when(userRepository.find(1)) thenReturn Option(defaultUser)

      class TestController extends Controller with BaseUserController{
        val userRepo = userRepository
      }

      val controller = new TestController
      val result: Future[Result] = controller.getUser(1L).apply(FakeRequest())
      
      val userJson: JsValue = contentAsJson(result)
      userJson mustBe Json.toJson(defaultUser)
    }
  }
}

Using dependency injection

We can make our controller depend on specific services, and all of this is configurable through the global object's getControllerInstance method by using a dependency injection library.

In this example, we have used Guice by adding it as a dependency for our project:

val appDependencies = Seq(
    ...
    "com.google.inject" % "guice" % "3.0",
    "javax.inject" % "javax.inject" % "1"
  )

Now, let's update the getControllerInstance method in the Global object:

object Global extends GlobalSettings {

  val injector = Guice.createInjector(new AbstractModule {
    protected def configure() {
      bind(classOf[UserRepo]).to(classOf[AnormUserRepo])
    }
  })

  override def getControllerInstance[A](controllerClass: Class[A]): A = injector.getInstance(controllerClass)
}

We now define UserController as a singleton that extends play.api.mvc.Controller and uses UserRepo, which is injected:

@Singleton
class UserController @Inject()(userRepo: UserRepo) extends Controller {

  implicit val userWrites = Json.writes[User]

  /* GET a specific user's details */
  def getUser(userId: Long) = Action {
    val u = userRepo.find(userId)
    if (u.isEmpty) {
      NoContent
    }
    else {
      Ok(Json.toJson(u))
    }
  }

}

We will also need to modify the routes file:

GET        /api/user/:userId        @controllers.UserController.getUser(userId:Long)

The @ symbol at the beginning of the method call indicates that the global object's getControllerInstance method should be used.

Note

If we do not add the @ suffix to the method name, it will search for an object with the UserController name and throw errors during compilation:

object UserController is not a member of package controllers
[error] Note: class UserController exists, but it has no companion object.
[error] GET        /api/user/:userId        controllers.UserController.getUser(userId:Long)

Finally, we can write a unit test using Specs2 as follows:

class UserControllerSpec extends Specification with Mockito {

  "UserController#getUser" should {
    "be valid" in {
      val userRepository = mock[AnormUserRepo]
      val defaultUser = User(Some(1), "loginId", Some("name"), Some("contact_no"), Some(20L), Some("address"))
      userRepository.find(1) returns Option(defaultUser)

      val controller = new UserController(userRepository)
      val result: Future[Result] = controller.getUser(1L).apply(FakeRequest())
      val userJson: JsValue = contentAsJson(result)
      
      userJson should be equalTo(Json.toJson(defaultUser))
    }
  }
}

Here, we mock AnormUserRepo and use this mock to generate a new instance of UserController.

The same test using ScalaTest will be as follows:

class UserControllerTest extends PlaySpec with Results with MockitoSugar {

  "UserController#getUser" should {
    "be valid" in {
      val userRepository = mock[AnormUserRepo]
      val defaultUser = User(Some(1), "loginId", Some("name"), Some("contact_no"), Some(20L), Some("address"))
      when(userRepository.find(1)) thenReturn Option(defaultUser)

      val controller = new UserController(userRepository)
      val result: Future[Result] = controller.getUser(1L).apply(FakeRequest())
      val userJson: JsValue = contentAsJson(result)
       
      userJson mustBe Json.toJson(defaultUser)
    }
  }
}

The following table summarizes the key differences in both these approaches, so that it's easier to decide which one suits your requirement in the best way:

Using traits for controllers

Using dependency injection

It requires defining and not just declaring all the methods to be supported by a controller in a trait.

It requires a controller to be defined as a singleton class and provides implementations for the global object's getControllerInstance method.

It does not require additional libraries.

It requires using a dependency injection library and provides flexibility to plug-in different classes in different application modes.

It requires defining an additional class for a controller, which extends a trait for testing.

It does not require any additional class definitions to test a controller, since a new instance can be instantiated from a singleton.

For more examples on dependency injection, refer to https://www.playframework.com/documentation/2.3.x/ScalaDependencyInjection.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.136.17.12