在 Play 2.1 和 Scala 中为文件上传编写测试用例

Posted

技术标签:

【中文标题】在 Play 2.1 和 Scala 中为文件上传编写测试用例【英文标题】:Writing a test case for file uploads in Play 2.1 and Scala 【发布时间】:2013-02-14 12:59:17 【问题描述】:

我找到了以下问题/答案:

Test MultipartFormData in Play 2.0 FakeRequest

但在 Play 2.1 中似乎情况发生了变化。我已经尝试像这样调整示例:

"Application" should 

"Upload Photo" in 
  running(FakeApplication()) 
    val data = new MultipartFormData(Map(), List(
        FilePart("qqfile", "message", Some("Content-Type: multipart/form-data"), 
            TemporaryFile(getClass().getResource("/test/photos/DSC03024.JPG").getFile()))
        ), List())
    val Some(result) = routeAndCall(FakeRequest(POST, "/admin/photo/upload", FakeHeaders(), data)) 
    status(result) must equalTo(CREATED)
    headers(result) must contain(LOCATION)
    contentType(result) must beSome("application/json")  

但是,每当我尝试运行请求时,都会收到空指针异常:

[error] ! Upload Photo
[error]     NullPointerException: null (PhotoManagementSpec.scala:25)
[error] test.PhotoManagementSpec$$anonfun$1$$anonfun$apply$3$$anonfun$apply$4.apply(PhotoManagementSpec.scala:28)
[error] test.PhotoManagementSpec$$anonfun$1$$anonfun$apply$3$$anonfun$apply$4.apply(PhotoManagementSpec.scala:25)
[error] play.api.test.Helpers$.running(Helpers.scala:40)
[error] test.PhotoManagementSpec$$anonfun$1$$anonfun$apply$3.apply(PhotoManagementSpec.scala:25)
[error] test.PhotoManagementSpec$$anonfun$1$$anonfun$apply$3.apply(PhotoManagementSpec.scala:25)

如果我尝试仅用路由替换已弃用的 routeAndCall(并删除结果周围的选项),我会收到一个编译错误,指出它无法将 MultipartFormData[TemporaryFile] 的实例写入 HTTP 响应。

在使用 Scala 的 Play 2.1 中设计此测试的正确方法是什么?


编辑:尝试修改代码以仅测试控制器:

"Application" should 

"Upload Photo" in 

   val data = new MultipartFormData(Map(), List(
   FilePart("qqfile", "message", Some("Content-Type: multipart/form-data"), 
    TemporaryFile(getClass().getResource("/test/photos/DSC03024.JPG").getFile()))
), List())

   val result = controllers.Photo.upload()(FakeRequest(POST, "/admin/photo/upload",FakeHeaders(),data))


   status(result) must equalTo(OK)
   contentType(result) must beSome("text/html")
   charset(result) must beSome("utf-8")
   contentAsString(result) must contain("Hello Bob")
  

但是我现在在结果周围的所有测试条件上都出现类型错误,如下所示:

[error]  found   : play.api.libs.iteratee.Iteratee[Array[Byte],play.api.mvc.Result]
[error]  required: play.api.mvc.Result

我不明白为什么要为映射到结果的字节数组获取交互器。这可能与我使用自定义正文解析器的方式有关吗?我的控制器的定义如下所示:

def upload = Action(CustomParsers.multipartFormDataAsBytes)  request =>

  request.body.file("qqfile").map  upload =>

使用这篇文章中的表单解析器:Pulling files from MultipartFormData in memory in Play2 / Scala

【问题讨论】:

一个类似的问题被问及回答:***.com/questions/15013177/… 我看到了问题和答案,但它仍然很混乱,没有很好地回答。您指向我已阅读的官方文档,其中不包括多部分表单数据。我实际上也想测试路线,但我想测试控制器就可以了。我仍然不明白如何将文件数据传递给名为“qqfile”的正文。你能用完整的答案编辑你的问题吗? 尝试仅测试控制器,但仍然遇到一些问题。上面列出了编辑。 我已经删除了该答案的第一部分(这是错误的)。 您现在遇到的错误是由您的上传方法方法调用方式引起的。您使用TemporaryFile 调用它,但您指定了multipartFormDataAsBytes 正文解析器。您应该使用 Array[Byte] 而不是 TemporaryFile 作为数据来调用它。 【参考方案1】:

Play 2.3 包含更新版本的 httpmime.jar,需要进行一些小的更正。基于 Marcus 的解决方案,使用 Play 的 Writeable 机制,同时保留了我的 Play 2.1 解决方案中的一些语法糖,这就是我想出的:

import scala.language.implicitConversions

import java.io.ByteArrayOutputStream, File

import org.apache.http.entity.ContentType
import org.apache.http.entity.mime.MultipartEntityBuilder
import org.apache.http.entity.mime.content._
import org.specs2.mutable.Specification

import play.api.http._
import play.api.libs.Files.TemporaryFile
import play.api.mvc.MultipartFormData.FilePart
import play.api.mvc.Codec, MultipartFormData
import play.api.test.Helpers._
import play.api.test.FakeApplication, FakeRequest

trait FakeMultipartUpload 
  implicit def writeableOf_multiPartFormData(implicit codec: Codec): Writeable[MultipartFormData[TemporaryFile]] = 
    val builder = MultipartEntityBuilder.create().setBoundary("12345678")

    def transform(multipart: MultipartFormData[TemporaryFile]): Array[Byte] = 
      multipart.dataParts.foreach  part =>
        part._2.foreach  p2 =>
          builder.addPart(part._1, new StringBody(p2, ContentType.create("text/plain", "UTF-8")))
        
      
      multipart.files.foreach  file =>
        val part = new FileBody(file.ref.file, ContentType.create(file.contentType.getOrElse("application/octet-stream")), file.filename)
        builder.addPart(file.key, part)
      

      val outputStream = new ByteArrayOutputStream
      builder.build.writeTo(outputStream)
      outputStream.toByteArray
    

    new Writeable[MultipartFormData[TemporaryFile]](transform, Some(builder.build.getContentType.getValue))
  

  /** shortcut for generating a MultipartFormData with one file part which more fields can be added to */
  def fileUpload(key: String, file: File, contentType: String): MultipartFormData[TemporaryFile] = 
    MultipartFormData(
      dataParts = Map(),
      files = Seq(FilePart[TemporaryFile](key, file.getName, Some(contentType), TemporaryFile(file))),
      badParts = Seq(),
      missingFileParts = Seq())
  

  /** shortcut for a request body containing a single file attachment */
  case class WrappedFakeRequest[A](fr: FakeRequest[A]) 
    def withFileUpload(key: String, file: File, contentType: String) = 
      fr.withBody(fileUpload(key, file, contentType))
    
  
  implicit def toWrappedFakeRequest[A](fr: FakeRequest[A]) = WrappedFakeRequest(fr)


class MyTest extends Specification with FakeMultipartUpload 
  "uploading" should 
    "be easier than this" in 
      running(FakeApplication()) 
        val uploadFile = new File("/tmp/file.txt")
        val req = FakeRequest(POST, "/upload/path").
          withFileUpload("image", uploadFile, "image/gif")
        val response = route(req).get
        status(response) must equalTo(OK)
      
    
  

【讨论】:

谢谢,在 2.3 上工作,为我省去了很多麻烦。 可以确认这也适用于 2.4。我想知道为什么 Helpers 中的 MultipartFormData 没有 Writeable...【参考方案2】:

根据各种邮件列表建议,我设法在 Play 2.1 中实现了这一点。这是我的做法:

import scala.language.implicitConversions

import java.io. ByteArrayOutputStream, File 

import org.apache.http.entity.mime.MultipartEntity
import org.apache.http.entity.mime.content. ContentBody, FileBody 
import org.specs2.mutable.Specification

import play.api.http.Writeable
import play.api.test. FakeApplication, FakeRequest 
import play.api.test.Helpers._

trait FakeMultipartUpload 
  case class WrappedFakeRequest[A](fr: FakeRequest[A]) 
    def withMultipart(parts: (String, ContentBody)*) = 
      // create a multipart form
      val entity = new MultipartEntity()
      parts.foreach  part =>
        entity.addPart(part._1, part._2)
      

      // serialize the form
      val outputStream = new ByteArrayOutputStream
      entity.writeTo(outputStream)
      val bytes = outputStream.toByteArray

      // inject the form into our request
      val headerContentType = entity.getContentType.getValue
      fr.withBody(bytes).withHeaders(CONTENT_TYPE -> headerContentType)
    

    def withFileUpload(fileParam: String, file: File, contentType: String) = 
      withMultipart(fileParam -> new FileBody(file, contentType))
    
  

  implicit def toWrappedFakeRequest[A](fr: FakeRequest[A]) = WrappedFakeRequest(fr)

  // override Play's equivalent Writeable so that the content-type header from the FakeRequest is used instead of application/octet-stream  
  implicit val wBytes: Writeable[Array[Byte]] = Writeable(identity, None)


class MyTest extends Specification with FakeMultipartUpload 
  "uploading" should 
    "be easier than this" in 
      running(FakeApplication()) 
        val uploadFile = new File("/tmp/file.txt")
        val req = FakeRequest(POST, "/upload/path").
          withFileUpload("image", uploadFile, "image/gif")
        val response = route(req).get
        status(response) must equalTo(OK)
      
    
  

【讨论】:

我会试试这个。我已经发布了一个我也必须工作的解决方案。 无法正常工作。获取异常IllegalArgumentException: protocol = http host = null。目前使用"commons-httpclient" % "commons-httpclient" % "3.1"。这是罪魁祸首还是其他原因?【参考方案3】:

我已修改 Alex 的代码以充当可更好地集成到 Play 2.2.2 中的 Writable

package test

import play.api.http._
import play.api.mvc.MultipartFormData.FilePart
import play.api.libs.iteratee._
import play.api.libs.Files.TemporaryFile
import play.api.mvc.Codec, MultipartFormData 
import java.io.FileInputStream, ByteArrayOutputStream
import org.apache.commons.io.IOUtils
import org.apache.http.entity.mime.MultipartEntity
import org.apache.http.entity.mime.content._

object MultipartWriteable 

  /**
   * `Writeable` for multipart/form-data.
   *
   */
  implicit def writeableOf_multiPartFormData(implicit codec: Codec): Writeable[MultipartFormData[TemporaryFile]] = 

    val entity = new MultipartEntity()

    def transform(multipart: MultipartFormData[TemporaryFile]):Array[Byte] = 

      multipart.dataParts.foreach  part =>
        part._2.foreach  p2 =>
            entity.addPart(part._1, new StringBody(p2))
        
      

      multipart.files.foreach  file =>
        val part = new FileBody(file.ref.file, file.filename,     file.contentType.getOrElse("application/octet-stream"), null)
        entity.addPart(file.key, part)
      

      val outputStream = new ByteArrayOutputStream
      entity.writeTo(outputStream)
      val bytes = outputStream.toByteArray
      outputStream.close
      bytes
    

    new Writeable[MultipartFormData[TemporaryFile]](transform, Some(entity.getContentType.getValue))
  

这样就可以写成这样:

val filePart:MultipartFormData.FilePart[TemporaryFile] = MultipartFormData.FilePart(...)
val fileParts:Seq[MultipartFormData.FilePart[TemporaryFile]] = Seq(filePart)
val dataParts:Map[String, Seq[String]] = ...
val multipart = new MultipartFormData[TemporaryFile](dataParts, fileParts, List(), List())
val request = FakeRequest(POST, "/url", FakeHeaders(), multipart)

var result = route(request).get

【讨论】:

如果您在上面添加类似this 的内容,您实际上可以使用Play 的FakeRequest().withMultipartFormDataBody() 方法。奇怪的是它开箱即用。【参考方案4】:

按照 EEColor 的建议,我得到了以下工作:

"Upload Photo" in 


    val file = scala.io.Source.fromFile(getClass().getResource("/photos/DSC03024.JPG").getFile())(scala.io.Codec.ISO8859).map(_.toByte).toArray

    val data = new MultipartFormData(Map(), List(
    FilePart("qqfile", "DSC03024.JPG", Some("image/jpeg"),
        file)
    ), List())

    val result = controllers.Photo.upload()(FakeRequest(POST, "/admin/photos/upload",FakeHeaders(),data))

    status(result) must equalTo(CREATED)
    headers(result) must haveKeys(LOCATION)
    contentType(result) must beSome("application/json")      


  

【讨论】:

值得指出的是,这个解决方案绕过了 Play 的路由器和序列化,而来自 Alex 的解决方案没有。 对我来说,这个仍然不工作,而 Alex 的版本工作并且对我来说看起来更干净。你可以重复使用它。您的版本抱怨没有 Array[Byte] Writer...【参考方案5】:

这是我的 Writeable[AnyContentAsMultipartFormData] 版本:

import java.io.File

import play.api.http.HeaderNames, Writeable
import play.api.libs.Files.TemporaryFile
import play.api.mvc.MultipartFormData.FilePart
import play.api.mvc.AnyContentAsMultipartFormData, Codec, MultipartFormData

object MultipartFormDataWritable 
  val boundary = "--------ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"

  def formatDataParts(data: Map[String, Seq[String]]) = 
    val dataParts = data.flatMap  case (key, values) =>
      values.map  value =>
        val name = s""""$key""""
        s"--$boundary\r\n$HeaderNames.CONTENT_DISPOSITION: form-data; name=$name\r\n\r\n$value\r\n"
      
    .mkString("")
    Codec.utf_8.encode(dataParts)
  

  def filePartHeader(file: FilePart[TemporaryFile]) = 
    val name = s""""$file.key""""
    val filename = s""""$file.filename""""
    val contentType = file.contentType.map  ct =>
      s"$HeaderNames.CONTENT_TYPE: $ct\r\n"
    .getOrElse("")
    Codec.utf_8.encode(s"--$boundary\r\n$HeaderNames.CONTENT_DISPOSITION: form-data; name=$name; filename=$filename\r\n$contentType\r\n")
  

  val singleton = Writeable[MultipartFormData[TemporaryFile]](
    transform =  form: MultipartFormData[TemporaryFile] =>
      formatDataParts(form.dataParts) ++
        form.files.flatMap  file =>
          val fileBytes = Files.readAllBytes(Paths.get(file.ref.file.getAbsolutePath))
          filePartHeader(file) ++ fileBytes ++ Codec.utf_8.encode("\r\n")
         ++
        Codec.utf_8.encode(s"--$boundary--")
    ,
    contentType = Some(s"multipart/form-data; boundary=$boundary")
  )


implicit val anyContentAsMultipartFormWritable: Writeable[AnyContentAsMultipartFormData] = 
  MultipartFormDataWritable.singleton.map(_.mdf)

改编自(并修复了一些错误):https://github.com/jroper/playframework/blob/multpart-form-data-writeable/framework/src/play/src/main/scala/play/api/http/Writeable.scala#L108

如果您有兴趣,请在此处查看整个帖子:http://tech.fongmun.com/post/125479939452/test-multipartformdata-in-play

【讨论】:

【参考方案6】:

对我来说,这个问题的最佳解决方案是Alex Varju one

这是为 Play 2.5 更新的版本:

object FakeMultipartUpload 
  implicit def writeableOf_multiPartFormData(implicit codec: Codec): Writeable[AnyContentAsMultipartFormData] = 
    val builder = MultipartEntityBuilder.create().setBoundary("12345678")

    def transform(multipart: AnyContentAsMultipartFormData): ByteString = 
      multipart.mdf.dataParts.foreach  part =>
        part._2.foreach  p2 =>
          builder.addPart(part._1, new StringBody(p2, ContentType.create("text/plain", "UTF-8")))
        
      
      multipart.mdf.files.foreach  file =>
        val part = new FileBody(file.ref.file, ContentType.create(file.contentType.getOrElse("application/octet-stream")), file.filename)
        builder.addPart(file.key, part)
      

      val outputStream = new ByteArrayOutputStream
      builder.build.writeTo(outputStream)
      ByteString(outputStream.toByteArray)
    

    new Writeable(transform, Some(builder.build.getContentType.getValue))
  

【讨论】:

【参考方案7】:

在 Play 2.6.x 中,您可以通过以下方式编写测试用例来测试文件上传 API:

class HDFSControllerTest extends Specification 
  "HDFSController" should 
    "return 200 Status for file Upload" in new WithApplication 

      val tempFile = SingletonTemporaryFileCreator.create("txt","csv")
      tempFile.deleteOnExit()

      val data = new MultipartFormData[TemporaryFile](Map(),
      List(FilePart("metadata", "text1.csv", Some("text/plain"), tempFile)), List())

      val res: Option[Future[Result]] = route(app, FakeRequest(POST, "/api/hdfs").withMultipartFormDataBody(data))
      print(contentAsString(res.get))
      res must beSome.which(status(_) == OK)
   
  

【讨论】:

【参考方案8】:

使 Alex 的版本与 Play 2.8 兼容

import akka.util.ByteString
import java.io.ByteArrayOutputStream
import org.apache.http.entity.mime.content.StringBody
import org.apache.http.entity.ContentType
import org.apache.http.entity.mime.content.FileBody
import org.apache.http.entity.mime.MultipartEntityBuilder
import play.api.http.Writeable
import play.api.libs.Files.TemporaryFile
import play.api.mvc.Codec
import play.api.mvc.MultipartFormData
import play.api.mvc.MultipartFormData.FilePart
import play.api.test.FakeRequest

trait FakeMultipartUpload 

  implicit def writeableOf_multiPartFormData(
    implicit codec: Codec
  ): Writeable[MultipartFormData[TemporaryFile]] = 
    val builder = MultipartEntityBuilder.create().setBoundary("12345678")

    def transform(multipart: MultipartFormData[TemporaryFile]): ByteString = 
      multipart.dataParts.foreach  part =>
        part._2.foreach  p2 =>
          builder.addPart(part._1, new StringBody(p2, ContentType.create("text/plain", "UTF-8")))
        
      
      multipart.files.foreach  file =>
        val part = new FileBody(
          file.ref.file,
          ContentType.create(file.contentType.getOrElse("application/octet-stream")),
          file.filename
        )
        builder.addPart(file.key, part)
      

      val outputStream = new ByteArrayOutputStream
      builder.build.writeTo(outputStream)
      ByteString(outputStream.toByteArray)
    

    new Writeable(transform, Some(builder.build.getContentType.getValue))
  

  /** shortcut for generating a MultipartFormData with one file part which more fields can be added to */
  def fileUpload(
    key: String,
    file: TemporaryFile,
    contentType: String
  ): MultipartFormData[TemporaryFile] = 
    MultipartFormData(
      dataParts = Map(),
      files = Seq(FilePart[TemporaryFile](key, file.file.getName, Some(contentType), file)),
      badParts = Seq()
    )
  

  /** shortcut for a request body containing a single file attachment */
  case class WrappedFakeRequest[A](fr: FakeRequest[A]) 
    def withFileUpload(key: String, file: TemporaryFile, contentType: String) = 
      fr.withBody(fileUpload(key, file, contentType))
    
  
  implicit def toWrappedFakeRequest[A](fr: FakeRequest[A]) = WrappedFakeRequest(fr)

【讨论】:

以上是关于在 Play 2.1 和 Scala 中为文件上传编写测试用例的主要内容,如果未能解决你的问题,请参考以下文章

在 Play Framework 2.4 中为 Scala 实现 CORS

在 Play Framework 2.4 中为 Scala 实现 Akka

在 Google Play 中为 aab 映射文件

在 Play2 / Scala 中从内存中的 MultipartFormData 中提取文件

使用Iteratees使用Play Scala将文件直接上传到S3 chunk-by-chunk

Play 2.1 框架未检测到进化变化