Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I unit test a controller in play framework 2 scala

Say I've got a controller with an action that receives two parameters.

It invokes two services, one with each parameter, the services both return strings

each of those strings are passed as arguments to a template

the result is passed to Ok and returned.

I want to write a simple unit test that ensures: 1 - The correct services are invoked with the correct parameters 2 - The return values from the services are passed to the correct attributes of the template

What is the best way to do that?

like image 755
The Trav Avatar asked Jul 09 '13 11:07

The Trav


People also ask

How do you write a controller unit test?

Writing a Unit Test for REST Controller First, we need to create Abstract class file used to create web application context by using MockMvc and define the mapToJson() and mapFromJson() methods to convert the Java object into JSON string and convert the JSON string into Java object.


2 Answers

Using Mockito with Specs2, I mock services to verify their method calls.

My controller is instantiated by Spring. That allows me to treat it is as a class instead of object. => That is essential to make controller testable. Here an example:

@Controller
class MyController @Autowired()(val myServices: MyServices) extends Controller

To enable Spring for controllers, you have to define a Global object, as the Play! documentation explains:

object Global extends GlobalSettings {

  val context = new ClassPathXmlApplicationContext("application-context.xml")

  override def getControllerInstance[A](controllerClass: Class[A]): A = {
    context.getBean(controllerClass)
  }
}

My unit test doesn't need Spring; I just pass collaborators (mocks) to constructor.

However, concerning the rendered template, I test only for the type of result (Ok, BadRequest, Redirection etc...). Indeed, I noticed it's not easy at all to make my test scan the whole rendered template in details (parameters sent to it etc..), with only unit testing.

Thus, in order to assert that the right template is called with the right arguments, I trust my acceptance tests running Selenium, or a possible functional test, if you prefer, to scan for the whole expected result.

2 - The return values from the services are passed to the correct attributes of the template

It's pretty easy to check for that..How? By trusting compiler! Prefer to pass some custom types to your template instead of simple primitives for instance: phone: String would become: phone: Phone. (a simple value object). Therefore, no fear to pass the attributes in a non-expected order to your template (in unit test or real production code). Compiler indeed will warn.

Here's an example of one of my unit test (simplified) using specs2: (You will note the use of a wrapper: WithFreshMocks). This case class would allow to refresh all variables (mocks in this case) test after test. Thus a good way to reset mocks.

    class MyControllerSpec extends Specification with Mockito {

      def is =
        "listAllCars should retrieve all cars" ! WithFreshMocks().listAllCarsShouldRetrieveAllCars

      case class WithFreshMocks() {

        val myServicesMock = mock[MyServices]
        val myController = new MyController(myServicesMock)

        def listAllCarsShouldRetrieveAllCars = {
          val FakeGetRequest = FakeRequest() //fakeRequest needed by controller
          mockListAllCarsAsReturningSomeCars()
          val result = myController.listAllCars(FakeGetRequest).asInstanceOf[PlainResult] //passing fakeRequest to simulate a true request
          assertOkResult(result).
            and(there was one(myServicesMock).listAllCars()) //verify that there is one and only one call of listAllCars. If listAllCars would take any parameters that you expected to be called, you could have precise them.
        }

        private def mockListAllCarsAsReturningSomeCars() { 
           myServicesMock.listAllCars() returns List[Cars](Car("ferrari"), Car("porsche"))
        }

        private def assertOkResult(result: PlainResult) = result.header.status must_== 200

       }
like image 51
Mik378 Avatar answered Sep 29 '22 12:09

Mik378


So, I came up with a cake pattern and mockito based solution:

given the service:

trait Service {
  def indexMessage : String
}

trait ServiceImpl {
  def indexMessage = {
    "Hello world"
  }
}

Then the controller looks like:

object Application extends ApplicationController
                   with ServiceImpl  {
  def template = views.html.index.apply
}

trait ApplicationController extends Controller
                            with Service {
  def template: (String) => play.api.templates.Html

  def index = Action {
    Ok(template("controller got:" + indexMessage))
  }
}

And the test looks like:

class ApplicationControllerSpec extends Specification with Mockito {
  "cake ApplicationController" should {
      "return OK with the results of the service invocation" in {
        val expectedMessage = "Test Message"
        val m = mock[(String) => play.api.templates.Html]

        object ApplicationControllerSpec extends ApplicationController {
          def indexMessage = expectedMessage
          def template = m
        }

        val response = ApplicationControllerSpec.index(FakeRequest())

        status(response) must equalTo(OK)
        there was one(m).apply(Matchers.eq("controller got:" + expectedMessage))
      }
  }
}

I had a lot of trouble getting Mockito working.
It requires an extra dependency and I had a lot of trouble working out how to use the matchers in scala (I'm quite comfortable using it in java)

Ultimately I think the above answer is better, avoid using String and other primitive types where you can wrap them in task specific types, then you get compiler warnings.

Also I would generally avoid doing things like the "controller got:" prefixing in the controller.

It's there in this case so I can verify that it went through, in the real world that should be done by some other component (controllers are just for plumbing IMO)

like image 23
The Trav Avatar answered Sep 29 '22 11:09

The Trav