【问题标题】:Writing a unit test for Play websockets为 Play websockets 编写单元测试
【发布时间】:2016-05-13 20:08:48
【问题描述】:

我正在使用 websocket 开发一个 Scala + Play 应用程序。我有一个简单的网络套接字定义如下:

def indexWS =  WebSocket.using[String] { request =>

val out = Enumerator("Hello!")
val in = Iteratee.foreach[String](println).map { _ =>
  println("Disconnected")
}


(in,out)
}

我已使用 Chrome 的控制台验证了此功能。我遇到的问题是尝试为此编写单元测试。目前我有这个:

"send awk for websocket connection" in {
  running(FakeApplication()){
    val js = route(FakeRequest(GET,"/WS")).get

    status(js) must equalTo (OK)
    contentType(js) must beSome.which(_ == "text/javascript")
  }
}

但是,在 Play 控制台中运行测试时,我收到此错误,其中第 35 行对应于此行 'val js = route(FakeRequest(GET,"/WS")).get':

NoSuchElementException: None.get (ApplicationSpec.scala:35)

我找不到单元测试 scala/play websockets 的好例子,并且对如何正确编写此测试感到困惑。

【问题讨论】:

  • 您找到解决问题的方法了吗?我目前正在尝试在 Play 2 中测试相同类型的东西,似乎基本的 FakeRequest 返回的结果不包含任何内容。
  • 这里也一样!找到解决方案了吗?来自@anquegi 的回答不被接受,这表明它无能为力。

标签: unit-testing scala playframework-2.0


【解决方案1】:

受 bruce-lowe 的回答启发,这里是 Hookup 的替代示例:

import java.net.URI
import io.backchat.hookup._
import org.specs2.mutable._
import play.api.test._
import scala.collection.mutable.ListBuffer

class ApplicationSpec extends Specification {

  "Application" should {

    "Test websocket" in new WithServer(port = 9000) {
      val hookupClient = new DefaultHookupClient(HookupClientConfig(URI.create("ws://localhost:9000/ws"))) {
        val messages = ListBuffer[String]()

        def receive = {
          case Connected =>
            println("Connected")

          case Disconnected(_) =>
            println("Disconnected")

          case JsonMessage(json) =>
            println("Json message = " + json)

          case TextMessage(text) =>
            messages += text
            println("Text message = " + text)
        }

        connect() onSuccess {
          case Success => send("Hello Server")
        }
      }

      hookupClient.messages.contains("Hello Client") must beTrue.eventually
    }

  }

}

该示例假设 websocket 参与者将回复 "Hello Client" 文本。

要包含该库,请将此行添加到libraryDependencies 中的build.sbt

"io.backchat.hookup" %% "hookup" % "0.4.2"

【讨论】:

    【解决方案2】:

    回答这个有点晚,但如果它有用,这里是我为我的 Websockets 编写测试的方法。它使用这里的库 (https://github.com/TooTallNate/Java-WebSocket)

    import org.specs2.mutable._
    import play.api.test.Helpers._
    import play.api.test._
    
    class ApplicationSpec extends Specification {
    
    "Application" should {
    
        "work" in {
          running(TestServer(9000)) {
    
            val clientInteraction = new ClientInteraction()
    
            clientInteraction.client.connectBlocking()
            clientInteraction.client.send("Hello Server")
    
            eventually {
              clientInteraction.messages.contains("Hello Client")
            }
          }  
        }
      }
    }
    

    还有一个用于存储所有消息/事件的小实用程序类(我相信您可以自己增强它以满足您的需求)

    import java.net.URI
    import org.java_websocket.client.WebSocketClient
    import org.java_websocket.drafts.Draft_17
    import org.java_websocket.handshake.ServerHandshake
    import collection.JavaConversions._
    import scala.collection.mutable.ListBuffer
    
    class ClientInteraction {
    
      val messages = ListBuffer[String]()
    
      val client = new     WebSocketClient(URI.create("ws://localhost:9000/wsWithActor"),
        new Draft_17(), Map("HeaderKey1" -> "HeaderValue1"), 0) {
    
        def onError(p1: Exception) {
          println("onError")
        }
    
        def onMessage(message: String) {
          messages += message
          println("onMessage, message = " + message)
        }
    
        def onClose(code: Int, reason: String, remote: Boolean) {                     
          println("onClose")
        }
    
        def onOpen(handshakedata: ServerHandshake) {
          println("onOpen")
        }
      }
    }
    

    这是在我的 SBT 文件中

    libraryDependencies ++= Seq(
      ws,
      "org.java-websocket" % "Java-WebSocket" % "1.3.0",
      "org.specs2" %% "specs2-core" % "3.7" % "test"
    )
    

    (这里有一个示例程序https://github.com/BruceLowe/play-with-websockets 有一个测试)

    【讨论】:

      【解决方案3】:

      我认为您可以查看site,它有一个关于使用 Spec 测试 websockets 的非常好的示例

      这是来自 typesafe 的示例:

      /*
       * Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
       */
      package play.it.http.websocket
      
      import play.api.test._
      import play.api.Application
      import scala.concurrent.{Future, Promise}
      import play.api.mvc.{Handler, Results, WebSocket}
      import play.api.libs.iteratee._
      import java.net.URI
      import org.jboss.netty.handler.codec.http.websocketx._
      import org.specs2.matcher.Matcher
      import akka.actor.{ActorRef, PoisonPill, Actor, Props}
      import play.mvc.WebSocket.{Out, In}
      import play.core.Router.HandlerDef
      import java.util.concurrent.atomic.AtomicReference
      import org.jboss.netty.buffer.ChannelBuffers
      
      object WebSocketSpec extends PlaySpecification with WsTestClient {
      
        sequential
      
        def withServer[A](webSocket: Application => Handler)(block: => A): A = {
          val currentApp = new AtomicReference[FakeApplication]
          val app = FakeApplication(
            withRoutes = {
              case (_, _) => webSocket(currentApp.get())
            }
          )
          currentApp.set(app)
          running(TestServer(testServerPort, app))(block)
        }
      
        def runWebSocket[A](handler: (Enumerator[WebSocketFrame], Iteratee[WebSocketFrame, _]) => Future[A]): A = {
          val innerResult = Promise[A]()
          WebSocketClient { client =>
            await(client.connect(URI.create("ws://localhost:" + testServerPort + "/stream")) { (in, out) =>
              innerResult.completeWith(handler(in, out))
            })
          }
          await(innerResult.future)
        }
      
        def textFrame(matcher: Matcher[String]): Matcher[WebSocketFrame] = beLike {
          case t: TextWebSocketFrame => t.getText must matcher
        }
      
        def closeFrame(status: Int = 1000): Matcher[WebSocketFrame] = beLike {
          case close: CloseWebSocketFrame => close.getStatusCode must_== status
        }
      
        def binaryBuffer(text: String) = ChannelBuffers.wrappedBuffer(text.getBytes("utf-8"))
      
        /**
         * Iteratee getChunks that invokes a callback as soon as it's done.
         */
        def getChunks[A](chunks: List[A], onDone: List[A] => _): Iteratee[A, List[A]] = Cont {
          case Input.El(c) => getChunks(c :: chunks, onDone)
          case Input.EOF =>
            val result = chunks.reverse
            onDone(result)
            Done(result, Input.EOF)
          case Input.Empty => getChunks(chunks, onDone)
        }
      
        /*
         * Shared tests
         */
        def allowConsumingMessages(webSocket: Application => Promise[List[String]] => Handler) = {
          val consumed = Promise[List[String]]()
          withServer(app => webSocket(app)(consumed)) {
            val result = runWebSocket { (in, out) =>
              Enumerator(new TextWebSocketFrame("a"), new TextWebSocketFrame("b"), new CloseWebSocketFrame(1000, "")) |>>> out
              consumed.future
            }
            result must_== Seq("a", "b")
          }
        }
      
        def allowSendingMessages(webSocket: Application => List[String] => Handler) = {
          withServer(app => webSocket(app)(List("a", "b"))) {
            val frames = runWebSocket { (in, out) =>
              in |>>> Iteratee.getChunks[WebSocketFrame]
            }
            frames must contain(exactly(
              textFrame(be_==("a")),
              textFrame(be_==("b")),
              closeFrame()
            ).inOrder)
          }
        }
      
        def cleanUpWhenClosed(webSocket: Application => Promise[Boolean] => Handler) = {
          val cleanedUp = Promise[Boolean]()
          withServer(app => webSocket(app)(cleanedUp)) {
            runWebSocket { (in, out) =>
              out.run
              cleanedUp.future
            } must beTrue
          }
        }
      
        def closeWhenTheConsumerIsDone(webSocket: Application => Handler) = {
          withServer(app => webSocket(app)) {
            val frames = runWebSocket { (in, out) =>
              Enumerator[WebSocketFrame](new TextWebSocketFrame("foo")) |>> out
              in |>>> Iteratee.getChunks[WebSocketFrame]
            }
            frames must contain(exactly(
              closeFrame()
            ))
          }
        }
      
        def allowRejectingTheWebSocketWithAResult(webSocket: Application => Int => Handler) = {
          withServer(app => webSocket(app)(FORBIDDEN)) {
            implicit val port = testServerPort
            await(wsUrl("/stream").withHeaders(
              "Upgrade" -> "websocket",
              "Connection" -> "upgrade"
            ).get()).status must_== FORBIDDEN
          }
        }
      
        "Plays WebSockets" should {
          "allow consuming messages" in allowConsumingMessages { _ => consumed =>
            WebSocket.using[String] { req =>
              (getChunks[String](Nil, consumed.success _), Enumerator.empty)
            }
          }
      
          "allow sending messages" in allowSendingMessages { _ => messages =>
            WebSocket.using[String] { req =>
              (Iteratee.ignore, Enumerator.enumerate(messages) >>> Enumerator.eof)
            }
          }
      
          "close when the consumer is done" in closeWhenTheConsumerIsDone { _ =>
            WebSocket.using[String] { req =>
              (Iteratee.head, Enumerator.empty)
            }
          }
      
          "clean up when closed" in cleanUpWhenClosed { _ => cleanedUp =>
            WebSocket.using[String] { req =>
              (Iteratee.ignore, Enumerator.empty[String].onDoneEnumerating(cleanedUp.success(true)))
            }
          }
      
          "allow rejecting a websocket with a result" in allowRejectingTheWebSocketWithAResult { _ => statusCode =>
            WebSocket.tryAccept[String] { req =>
              Future.successful(Left(Results.Status(statusCode)))
            }
          }
      
          "allow handling a WebSocket with an actor" in {
      
            "allow consuming messages" in allowConsumingMessages { implicit app => consumed =>
              WebSocket.acceptWithActor[String, String] { req => out =>
                Props(new Actor() {
                  var messages = List.empty[String]
                  def receive = {
                    case msg: String =>
                      messages = msg :: messages
                  }
                  override def postStop() = {
                    consumed.success(messages.reverse)
                  }
                })
              }
            }
      
            "allow sending messages" in allowSendingMessages { implicit app => messages =>
              WebSocket.acceptWithActor[String, String] { req => out =>
                Props(new Actor() {
                  messages.foreach { msg =>
                    out ! msg
                  }
                  out ! PoisonPill
                  def receive = PartialFunction.empty
                })
              }
            }
      
            "close when the consumer is done" in closeWhenTheConsumerIsDone { implicit app =>
            WebSocket.acceptWithActor[String, String] { req => out =>
                Props(new Actor() {
                  out ! PoisonPill
                  def receive = PartialFunction.empty
                })
              }
            }
      
            "clean up when closed" in cleanUpWhenClosed { implicit app => cleanedUp =>
              WebSocket.acceptWithActor[String, String] { req => out =>
                Props(new Actor() {
                  def receive = PartialFunction.empty
                  override def postStop() = {
                    cleanedUp.success(true)
                  }
                })
              }
            }
      
            "allow rejecting a websocket with a result" in allowRejectingTheWebSocketWithAResult { implicit app => statusCode =>
              WebSocket.tryAcceptWithActor[String, String] { req =>
                Future.successful(Left(Results.Status(statusCode)))
              }
            }
      
            "aggregate text frames" in {
              val consumed = Promise[List[String]]()
              withServer(app => WebSocket.using[String] { req =>
                (getChunks[String](Nil, consumed.success _), Enumerator.empty)
              }) {
                val result = runWebSocket { (in, out) =>
                  Enumerator(
                    new TextWebSocketFrame("first"),
                    new TextWebSocketFrame(false, 0, "se"),
                    new ContinuationWebSocketFrame(false, 0, "co"),
                    new ContinuationWebSocketFrame(true, 0, "nd"),
                    new TextWebSocketFrame("third"),
                    new CloseWebSocketFrame(1000, "")) |>>> out
                  consumed.future
                }
                result must_== Seq("first", "second", "third")
              }
      
            }
      
            "aggregate binary frames" in {
              val consumed = Promise[List[Array[Byte]]]()
      
              withServer(app => WebSocket.using[Array[Byte]] { req =>
                (getChunks[Array[Byte]](Nil, consumed.success _), Enumerator.empty)
              }) {
                val result = runWebSocket { (in, out) =>
                  Enumerator(
                    new BinaryWebSocketFrame(binaryBuffer("first")),
                    new BinaryWebSocketFrame(false, 0, binaryBuffer("se")),
                    new ContinuationWebSocketFrame(false, 0, binaryBuffer("co")),
                    new ContinuationWebSocketFrame(true, 0, binaryBuffer("nd")),
                    new BinaryWebSocketFrame(binaryBuffer("third")),
                    new CloseWebSocketFrame(1000, "")) |>>> out
                  consumed.future
                }
                result.map(b => b.toSeq) must_== Seq("first".getBytes("utf-8").toSeq, "second".getBytes("utf-8").toSeq, "third".getBytes("utf-8").toSeq)
              }
            }
      
            "close the websocket when the buffer limit is exceeded" in {
              withServer(app => WebSocket.using[String] { req =>
                (Iteratee.ignore, Enumerator.empty)
              }) {
                val frames = runWebSocket { (in, out) =>
                  Enumerator[WebSocketFrame](
                    new TextWebSocketFrame(false, 0, "first frame"),
                    new ContinuationWebSocketFrame(true, 0, new String(Array.range(1, 65530).map(_ => 'a')))
                  ) |>> out
                  in |>>> Iteratee.getChunks[WebSocketFrame]
                }
                frames must contain(exactly(
                  closeFrame(1009)
                ))
              }
            }
      
          }
      
          "allow handling a WebSocket in java" in {
      
            import play.core.Router.HandlerInvokerFactory
            import play.core.Router.HandlerInvokerFactory._
            import play.mvc.{ WebSocket => JWebSocket, Results => JResults }
            import play.libs.F
      
            implicit def toHandler[J <: AnyRef](javaHandler: J)(implicit factory: HandlerInvokerFactory[J]): Handler = {
              val invoker = factory.createInvoker(
                javaHandler,
                new HandlerDef(javaHandler.getClass.getClassLoader, "package", "controller", "method", Nil, "GET", "", "/stream")
              )
              invoker.call(javaHandler)
            }
      
            "allow consuming messages" in allowConsumingMessages { _ => consumed =>
              new JWebSocket[String] {
                @volatile var messages = List.empty[String]
                def onReady(in: In[String], out: Out[String]) = {
                  in.onMessage(new F.Callback[String] {
                    def invoke(msg: String) = messages = msg :: messages
                  })
                  in.onClose(new F.Callback0 {
                    def invoke() = consumed.success(messages.reverse)
                  })
                }
              }
            }
      
            "allow sending messages" in allowSendingMessages { _ => messages =>
              new JWebSocket[String] {
                def onReady(in: In[String], out: Out[String]) = {
                  messages.foreach { msg =>
                    out.write(msg)
                  }
                  out.close()
                }
              }
            }
      
            "clean up when closed" in cleanUpWhenClosed { _ => cleanedUp =>
              new JWebSocket[String] {
                def onReady(in: In[String], out: Out[String]) = {
                  in.onClose(new F.Callback0 {
                    def invoke() = cleanedUp.success(true)
                  })
                }
              }
            }
      
            "allow rejecting a websocket with a result" in allowRejectingTheWebSocketWithAResult { _ => statusCode =>
              JWebSocket.reject[String](JResults.status(statusCode))
            }
      
            "allow handling a websocket with an actor" in allowSendingMessages { _ => messages =>
              JWebSocket.withActor[String](new F.Function[ActorRef, Props]() {
                def apply(out: ActorRef) = {
                  Props(new Actor() {
                    messages.foreach { msg =>
                      out ! msg
                    }
                    out ! PoisonPill
                    def receive = PartialFunction.empty
                  })
                }
              })
            }
          }
        }
      }
      

      【讨论】:

      • 是否有一个示例显示了一个简单操作的测试,例如: def websocket(id: Int) = WebSocket.tryAcceptWithActor[JsValue, JsValue] { request => ... }
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2017-08-16
      • 1970-01-01
      • 2018-01-17
      • 2012-01-06
      • 2013-06-02
      • 2019-03-17
      相关资源
      最近更新 更多