ivanorone
ivanorone

Reputation: 449

Unit testing play controller actions that have json parsers

I have a controller action that I would like to test however I am getting the error below. I feel like there is something small that I could be missing but can't quite figure out what it is.I wrote out some small code to help illustrate the problem.

Abit of more context, I would like to be able to supply mock dependencies to the controller thats why I am manaully taking over its instantiation.

java.lang.UnsupportedOperationException: NoMaterializer does not provide an ExecutionContext
[info]   at play.api.test.NoMaterializer$.executionContext(Helpers.scala:661)
[info]   at play.api.mvc.PlayBodyParsers.$anonfun$enforceMaxLength$1(BodyParsers.scala:866)
[info]   at akka.stream.impl.Compose.apply(TraversalBuilder.scala:164)
[info]   at akka.stream.impl.PhasedFusingActorMaterializer.materialize(PhasedFusingActorMaterializer.scala:488)
[info]   at akka.stream.impl.PhasedFusingActorMaterializer.materialize(PhasedFusingActorMaterializer.scala:424)
[info]   at akka.stream.impl.PhasedFusingActorMaterializer.materialize(PhasedFusingActorMaterializer.scala:415)
[info]   at akka.stream.scaladsl.RunnableGraph.run(Flow.scala:496)

Below is the controller I wrote to help illustrate the problem

case class Location(lat: Double, long: Double)

object Location {

  implicit val locationReads: Reads[Location] = (
    (JsPath \ "lat").read[Double](min(-90.0) keepAnd max(90.0)) and
      (JsPath \ "long").read[Double](min(-180.0) keepAnd max(180.0))
    ) (Location.apply _)
}

class HomeController @Inject()(
    cc: ControllerComponents,
    mat: Materializer
)(implicit val ec: ExecutionContext) extends AbstractController(cc) {


  def savePlace: Action[JsValue] = Action(parse.json) { request: Request[JsValue] =>
    val placeResult = request.body.validate[Location]
    placeResult.fold(
      errors => {
        BadRequest(Json.obj("status" -> "KO", "message" -> JsError.toJson(errors)))
      },
      place => {
        Ok(Json.obj("status" -> "OK", "message" -> "Location saved"))
      }
    )
  }
}

and the test code

class HomeControllerSpec extends PlaySpec with Injecting with Inside with GuiceOneAppPerSuite with BeforeAndAfterEach with Results with BeforeAndAfterAll {
  implicit lazy override val app: play.api.Application = new GuiceApplicationBuilder().configure().build()
  implicit lazy val materializer: Materializer = app.materializer
  implicit lazy val components = Helpers.stubControllerComponents()

  "savePlace" must {

    val api = new HomeController(components, mat = materializer)

    val payload = Json.obj(
      "location" -> Json.obj("lat" -> 51.235685, "lng" -> -1.309197)
    )

    "just demo" in {
      val request = FakeRequest(POST, "/location").withJsonBody(payload)

      val result = call(api.savePlace, request)
      status(result) mustBe UNAUTHORIZED
    }
  }
}

I am using play 2.6.3

Upvotes: 5

Views: 1452

Answers (3)

arteme
arteme

Reputation: 21

There are cases when you do not want (or cannot) use dependency injection.

The error comes from the fact that stubControllerComponents has playBodyParsers: PlayBodyParsers = stubPlayBodyParsers(NoMaterializer), so it is enough to change components = ... above to:

implicit lazy val components = Helpers.stubControllerComponents(
    playBodyParsers = stubPlayBodyParsers(materializer)
)

Upvotes: 0

Sam Upra
Sam Upra

Reputation: 737

Extend playspec with GuiceOneAppPerSuite in your test class. Now you will have an instance of an app that you can use to unit test this.

E.g.

class HomeSpec extends PlaySpec with GuiceOneAppPerSuite {

  "Home" should {

    "send 404 on a bad request" in {
      route(app, FakeRequest(GET, "/boum")).map(status(_)) mustBe Some(
        NOT_FOUND)
    }

    "send 200 on a good request" in {
      route(app, FakeRequest(GET, "/")).map(status(_)) mustBe Some(OK)
    }

  }
}

I am currently using this test for play 2.6.1. What this will do, is, you will have an instance of the whole app, so you can test all Controllers without having to instantiate one by one.

Upvotes: 0

ivanorone
ivanorone

Reputation: 449

I am answering this to benefit someone else who might face the same problem.

To solve the problem, instead of injecting stubbed controller components (as in Helpers.stubControllerComponents()) , I used inject[ControllerComponents].

Basically, to solve the problem the controller code would now look like this.

class HomeControllerSpec
  extends PlaySpec with Injecting with Inside with GuiceOneAppPerSuite with Results {
  implicit lazy override val app: play.api.Application = new GuiceApplicationBuilder().configure().build()
  implicit lazy val materializer = app.materializer

    implicit lazy val components = inject[ControllerComponents]

  "savePlace" must {

    val api = new HomeController(components)

    val payload = Json.obj("lat" -> 51.235685, "long" -> -1.309197)

    "just demo" in {
      val request = FakeRequest(POST, "/location").withJsonBody(payload)

      val result = call(api.savePlace, request)
      println(contentAsJson(result))
      status(result) mustBe UNAUTHORIZED

    }
  }
}

Upvotes: 2

Related Questions