寻找像TestFlow和TestSource类似的TestFlow

时间:2018-05-05 15:54:29

标签: scala sockets akka akka-stream

我正在编写一个类,它将Flow(表示一种套接字)作为构造函数参数,并允许通过返回Future来发送消息并异步等待相应的答案。例如:

class SocketAdapter(underlyingSocket: Flow[String, String, _]) {
    def sendMessage(msg: MessageType): Future[ResponseType]
}

这不一定是微不足道的,因为套接字流中可能有其他消息无关紧要,因此需要进行一些过滤。

为了测试课程,我需要提供类似于TestSinkTestSource的“TestFlow”。事实上,我可以通过组合两者来创建流程。然而,问题在于我只在物化时获得了实际的探测,并且在被测试的类中发生了物化。

问题类似于我在this question中描述的问题。如果我可以首先实现流程然后将其传递给客户端以连接到它,我的问题将得到解决。我再次考虑使用MergeHubBroadcastHub,我再次看到结果流的行为会有所不同,因为它不再是线性的。

也许我误解了Flow应该如何使用。为了在调用sendMessage()时将消息提供给流,我无论如何都需要某种Source。可能是Source.actorRef(...)Source.queue(...),因此我可以直接传递ActorRefSourceQueue。但是,我更喜欢这个选择取决于SocketAdapter类。当然,这也适用于Sink

使用流和套接字时,感觉这是一个相当常见的情况。如果无法像我需要的那样创建“TestFlow”,我也很满意如何改进我的设计并使其更易于测试。

更新:我浏览了文档,找到了SourceRefSinkRef。看起来这些可以解决我的问题,但我还不确定。在我的情况下使用它们是否合理,或者是否有任何缺点,例如测试中的不同行为与没有此类参考的生产相比?

2 个答案:

答案 0 :(得分:0)

间接答案

您的问题的性质表明您在测试时遇到的设计缺陷。以下答案并未解决您问题中的问题,但它演示了如何完全避免这种情况。

不要将业务逻辑与Akka代码混合

大概你需要测试你的Flow,因为你已经在物化中加入了大量的逻辑。让我们假设您正在为IO使用原始套接字。您的问题表明您的流程如下:

val socketFlow : Flow[String, String, _] = {
  val socket = new Socket(...)

  //business logic for IO
}

您的Flow需要一个复杂的测试框架,因为您的Flow本身也很复杂。

相反,您应该将逻辑分离为一个没有akka依赖关系的独立函数:

type MessageProcessor = MessageType => ResponseType

object BusinessLogic {
  val createMessageProcessor : (Socket) => MessageProcessor = {
    //business logic for IO
  } 
}

现在您的流程非常简单:

val socket : Socket = new Socket(...)

val socketFlow = Flow.map(BusinessLogic.createMessageProcessor(socket))

因此:您的单元测试可以专门用于createMessageProcessor,不需要测试akka Flow,因为它是一个简单的单板,围绕着独立测试的复杂逻辑。

不要使用Streams来实现1个元素的并发

您的设计的另一个大问题是SocketAdapter正在使用流来一次处理1条消息。这是非常浪费和不必要的(你试图用坦克杀死蚊子)。

鉴于分离的业务逻辑,您的适配器变得更加简单并且独立于akka:

class SocketAdapter(messageProcessor : MessageProcessor) {
  def sendMessage(msg: MessageType): Future[ResponseType] = Future {
    messageProcessor(msg)
  }
}

请注意,在某些情况下使用Future和在其他情况下使用Flow是多么容易,具体取决于需要。这是因为业务逻辑独立于任何并发框架。

答案 1 :(得分:0)

这是我使用SinkRefSourceRef

提出的
object TestFlow {

  def withProbes[In, Out](implicit actorSystem: ActorSystem,
                                   actorMaterializer: ActorMaterializer)
    :(Flow[In, Out, _], TestSubscriber.Probe[In], TestPublisher.Probe[Out]) = {
    val f = Flow.fromSinkAndSourceMat(TestSink.probe[In], TestSource.probe[Out])
            (Keep.both)

    val ((sinkRefFuture, (inProbe, outProbe)), sourceRefFuture) =
      StreamRefs.sinkRef[In]()
        .viaMat(f)(Keep.both)
        .toMat(StreamRefs.sourceRef[Out]())(Keep.both)
        .run()

    val sinkRef = Await.result(sinkRefFuture, 3.seconds)
    val sourceRef = Await.result(sourceRefFuture, 3.seconds)

    (Flow.fromSinkAndSource(sinkRef, sourceRef), inProbe, outProbe)
  }

}

这给了我一个我可以用两个探针完全控制的流程,但是我可以将它传递给后来连接源和接收器的客户端,所以它似乎解决了我的问题。

结果Flow应该只使用一次,因此它与常规Flow不同,后者是流程蓝图,可以多次实现。但是,这个限制适用于我正在嘲笑的Web套接字流,如here所述。

我仍然遇到的唯一问题是,ActorSystem在测试后终止时会记录一些警告。这似乎是由于SinkRefSourceRef引入的间接性。

更新:我使用SinkRef找到了一个没有SourceRefmapMaterializedValue()的更好的解决方案:

def withProbesFuture[In, Out](implicit actorSystem: ActorSystem,
                                       ec: ExecutionContext)
  : (Flow[In, Out, _],
         Future[(TestSubscriber.Probe[In], TestPublisher.Probe[Out])]) = {
    val (sinkPromise, sourcePromise) =
        (Promise[TestSubscriber.Probe[In]], Promise[TestPublisher.Probe[Out]])

    val flow =
      Flow
        .fromSinkAndSourceMat(TestSink.probe[In], TestSource.probe[Out])(Keep.both)
        .mapMaterializedValue { case (inProbe, outProbe) =>
          sinkPromise.success(inProbe)
          sourcePromise.success(outProbe)
          ()
        }

    val probeTupleFuture = sinkPromise.future
        .flatMap(sink => sourcePromise.future.map(source => (sink, source)))
    (flow, probeTupleFuture)
  }

当被测试的课程实现了流程时,Future已完成,我收到了测试探针。