我正在编写一个类,它将Flow(表示一种套接字)作为构造函数参数,并允许通过返回Future
来发送消息并异步等待相应的答案。例如:
class SocketAdapter(underlyingSocket: Flow[String, String, _]) {
def sendMessage(msg: MessageType): Future[ResponseType]
}
这不一定是微不足道的,因为套接字流中可能有其他消息无关紧要,因此需要进行一些过滤。
为了测试课程,我需要提供类似于TestSink
和TestSource
的“TestFlow”。事实上,我可以通过组合两者来创建流程。然而,问题在于我只在物化时获得了实际的探测,并且在被测试的类中发生了物化。
问题类似于我在this question中描述的问题。如果我可以首先实现流程然后将其传递给客户端以连接到它,我的问题将得到解决。我再次考虑使用MergeHub
和BroadcastHub
,我再次看到结果流的行为会有所不同,因为它不再是线性的。
也许我误解了Flow
应该如何使用。为了在调用sendMessage()
时将消息提供给流,我无论如何都需要某种Source
。可能是Source.actorRef(...)
或Source.queue(...)
,因此我可以直接传递ActorRef
或SourceQueue
。但是,我更喜欢这个选择取决于SocketAdapter
类。当然,这也适用于Sink
。
使用流和套接字时,感觉这是一个相当常见的情况。如果无法像我需要的那样创建“TestFlow”,我也很满意如何改进我的设计并使其更易于测试。
更新:我浏览了文档,找到了SourceRef
和SinkRef
。看起来这些可以解决我的问题,但我还不确定。在我的情况下使用它们是否合理,或者是否有任何缺点,例如测试中的不同行为与没有此类参考的生产相比?
答案 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)
这是我使用SinkRef
和SourceRef
:
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
在测试后终止时会记录一些警告。这似乎是由于SinkRef
和SourceRef
引入的间接性。
更新:我使用SinkRef
找到了一个没有SourceRef
和mapMaterializedValue()
的更好的解决方案:
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
已完成,我收到了测试探针。