12

I am currently in the process of writing some Specs2 tests for may Play Framework 2.2.x application which accepts MultipartFormData submissions as part of it's function.

I have successfully written other tests with text and JSON bodies using the following form:

"respond to POST JSON with description field present" in {
  running(FakeApplication()) {
    val response = route(FakeRequest(POST, "/submission.json").withJsonBody(toJson(Map("content" -> toJson("test-content"), "description" -> toJson("test-description"))))).get
    status(response) must equalTo(OK)
    contentType(response) must beSome.which(_ == "application/json")
    contentAsString(response) must contain(""""description":"test-description"""")
    contentAsString(response) must contain(""""content":"test-content"""")
  }
}

However, when I use the .withMultipartFormData method I get the following errors:

Cannot write an instance of play.api.mvc.AnyContentAsMultipartFormData to HTTP response. Try to define a Writeable[play.api.mvc.AnyContentAsMultipartFormData]
val response = route(FakeRequest(PUT,"/submission.json/1/files").withMultipartFormDataBody(data)).get
                    ^

The MultipartFormData test I have been attempting to debug is of the form:

"respond to file PUT form with details not specififed" in {
  running(FakeApplication()) {
     val basePath:String = Play.application.path.getCanonicalPath();
     val data:MultipartFormData[TemporaryFile] = MultipartFormData(Map[String,Seq[String]](),
                                    List(
                                        FilePart("file_upload","",Some("Content-Type: multipart/form-data"),TemporaryFile(new java.io.File(basePath + "/test-data/testUpload.jpg")))
                                    ), 
                                    List(), 
                                    List())


     val response = route(FakeRequest(PUT,"/submission.json/1/files").withMultipartFormDataBody(data)).get
     status(response) must equalTo(CREATED)
 }
}

Looking at the Play Framework documentation for the relevant version of the FakeRequest class I can't see too much to help me trace down the problem: play.api.test.FakeRequest

And in terms of other documentation on the matter it seems the Play Framework website and Google are rather lacking.

I have tried the following alternative means of attempting to test my MultipartFormData code:

However, I have not had any success with any of these approaches either.

Community
  • 1
  • 1
qt.cls
  • 133
  • 1
  • 5
  • 1
    This issue is that the test `Helpers` don't include an `implicit Writeable[play.api.mvc.AnyContentAsMultipartFormData]`. If you look at the Helpers api you'll see that `route` requires an `implicit Writeable` and there are some pre-baked ones included in the package. Unfortunately, `MultipartFormData` is not one of them. Check out [this related SO question](http://stackoverflow.com/a/24622059/1721762) for a possible solution (defines the missing `implicit`). – eharik May 07 '15 at 12:45

3 Answers3

7

Rather than testing in a FakeApplication which is slow and (in my experience) can be error-prone when tests are running in parallel, I've been unit testing my Multipart form upload handlers like this:

  1. Split out the Play wiring from your actual upload handling in your controller; e.g.:

    def handleUpload = Action(parse.multipartFormData) { implicit request =>
      doUpload(request)
    }
    
    def doUpload(request:Request[MultipartFormData[TemporaryFile]]) = {
      ...
    }
    

    (Where handleUpload is the method in your routes file that handles the POST)

  2. Now you've got an endpoint that's easier to get at, you can mock out your service layer to respond appropriately to good/bad requests, and inject the mock service into your controller under test (I won't show that here, there are a million different ways to do that)

  3. Now mock out a multipart request that will arrive at your doUpload method:

    val request= mock[Request[MultipartFormData[TemporaryFile]]]
    val tempFile = TemporaryFile("do_upload","spec")
    val fileName = "testFile.txt"
    val part = FilePart("key: String", fileName, None, tempFile)
    val files = Seq[FilePart[TemporaryFile]](part)
    val multipartBody = MultipartFormData(Map[String, Seq[String]](), files, Seq[BadPart](), Seq[MissingFilePart]())
    request.body returns multipartBody 
    
  4. And finally, you can call your doUpload method and verify functionality:

    val result = controller.doUpload(request)
    status(result) must beEqualTo(201)
    

By testing like this, you can quickly and easily test all the error-handling paths in your Controller (which is probably what you're trying to do after all) without the overhead of needing to start the entire application.

millhouse
  • 8,542
  • 4
  • 29
  • 39
  • 1
    A better design is to create a service that will handle the upload and send the multipart file to that service. Then you can unit test the service with all scenarios and there is no need to create extra method in Controller that is public. Besides that handleUpload method is not tested. – Robert Gabriel Dec 07 '17 at 10:53
  • 1
    All good points @RobertGabriel - `doUpload` could be declared `private [controllers]` but you're totally correct - it is definitely better to completely embrace Single Responsibility and have a service for this. – millhouse Dec 08 '17 at 00:30
5

In Play 2.5.x, it is easy to test file upload

  val file = new java.io.File("the.file")
  val part = FilePart[File](key = "thekey", filename = "the.file", contentType = None, ref = file)
  val request =  FakeRequest().withBody(
    MultipartFormData[File](dataParts = Map.empty, files = Seq(part), badParts = Nil)
  )
  val response = controller.create().apply(request)
  status(response) must beEqualTo(201)
  • This solved my problem of not having a Materializer. Thanks. I had a `could not find implicit value for parameter mat: akka.stream.Materializer` error which led me to use `GuiceOneAppPerSuite`. `GuiceOneAppPerSuite` has a `Materializer` but does not have an execution context. So my next error was `NoMaterializer does not provide an ExecutionContext`. That seemed like a dead end until I run into this suggestion. So I have a question why `val request = FakeRequest().withMultipartFormDataBody(multipartData)` does not work but `val request = FakeRequest().withBody(multipartData)` does? – gcaliari Aug 24 '18 at 09:07
2

(I've answered in the other thread: PlayFramework Testing: Uploading File in Fake Request Errors)

In short, you need a Writeable[AnyContentAsMultipartFormData], which turns MultipartFormData[TemporaryFile] into Array[Byte], and you can take it from here: http://tech.fongmun.com/post/125479939452/test-multipartformdata-in-play

Community
  • 1
  • 1
Tanin
  • 1,741
  • 1
  • 13
  • 18