Scala 在将未来消息与参与者消息混合时,确保测试中的消息顺序

Scala 在将未来消息与参与者消息混合时,确保测试中的消息顺序,scala,testing,akka,actor,akka-testkit,Scala,Testing,Akka,Actor,Akka Testkit,我正在测试一个使用asnycronousfuture-basedapi的actor。参与者使用管道模式在未来完成时向自己发送消息: import akka.pattern.pipe // ... // somewhere in the actor's receive method futureBasedApi.doSomething().pipeTo(self) 在我的测试中,我模拟了API,所以我通过承诺来控制未来的完成。但是,这与直接发送给参与者的其他消息交织在一起: myActor !

我正在测试一个使用asnycronousfuture-basedapi的actor。参与者使用管道模式在未来完成时向自己发送消息:

import akka.pattern.pipe
// ...

// somewhere in the actor's receive method
futureBasedApi.doSomething().pipeTo(self)
在我的测试中,我模拟了API,所以我通过承诺来控制未来的完成。但是,这与直接发送给参与者的其他消息交织在一起:

myActor ! Message("A")
promiseFromApiCall.success(Message("B"))
myActor ! Message("C")
现在我想知道我怎样才能保证演员接受和接受 在测试中处理消息A和C之间的消息B,因为消息B实际上是在另一个线程中发送的,所以我无法控制顺序 参与者的邮箱在其中接收消息

我考虑了几种可能的解决方案:

  • 在每条消息之后睡眠几毫秒以生成另一条消息 订单不大可能

  • 等待参与者确认每条消息,尽管 只有在测试时才需要确认

  • 将消息B直接发送给参与者,以模拟任务的完成 并编写一个单独的测试,以确保管道模式 正确使用(如果参与者愿意,上述测试不会失败 不通过管道将结果消息传递到自身)

我真的不喜欢这两个选项中的任何一个,但我倾向于使用最后一个 一个。有没有其他更好的方法可以在测试中强制执行特定的消息顺序


澄清:问题不在于如何处理生产过程中可能以随机顺序接收消息的事实。控制测试中的顺序对于确保参与者能够实际处理不同的消息顺序至关重要。

一个想法是在参与者中定义一个标志,指示参与者是否已接收到消息B。当参与者接收到消息C时,如果标志为假,参与者可以接收该消息C,然后在参与者收到消息B后将其取消播放。例如:

class MyActor extends Actor with Stash {

  def receiveBlock(seenMsgB: Boolean, seenMsgC: Boolean): Receive = {
    case MakeApiCall =>
      callExternalApi().mapTo[MessageB].pipeTo(self)

    case m: MessageB if seenMsgC => // assume msg C has been stashed
      unstashAll()
      // ...do something with msg B
      become(receiveBlock(true, seenMsgC)) // true, true
    case m: MessageB if !seenMsgC =>
      // ...do something with message B
      become(receiveBlock(true, seenMsgC)) // true, false

    case m: MessageC if seenMsgB =>
      // ...do something with message C
      context.become(receiveBlock(seenMsgB, true)) // true, true
    case m: MessageC if !seenMsgB =>
      stash()
      context.become(receiveBlock(seenMsgB, true)) // false, true

    case ...
  }

  def receive = receiveBlock(false, false)
}

如果订购邮件非常重要,那么您应该使用
询问
)返回
未来
,并将它们链接起来,即使您不期望参与者做出任何响应。

在阅读了更多关于akka的信息后,我终于找到了一个更好的解决方案:用我在测试中可以观察到的邮箱替换参与者邮箱。这样我就可以等到演员在我完成承诺后收到新的信息。然后才发送下一条消息。此
测试邮箱的代码在文章末尾给出

更新:在Akka类型中,这可以通过
行为接收器
非常优雅地实现。只需使用一个自定义拦截器包装测试中的
行为
,该拦截器转发所有消息和信号,但允许您观察它们。 下面给出了非类型化Akka的邮箱解决方案


参与者的配置如下所示:

actorUnderTest = system.actorOf(Props[MyActor]).withMailbox("testing-mailbox"))
myActor ! Message("A")
val nextMessage = TestingMailbox.nextMessage(actorUnderTest)
promiseFromApiCall.success(Message("B"))
Await.ready(nextMessage, 3.seconds)
myActor ! Message("C")
myActor ! Message("A")
receiveMessageAfter { promiseFromApiCall.success(Message("B")) }
myActor ! Message("C")
我必须通过提供以下配置来确保actor系统知道“测试邮箱”:

class MyTest extends TestKit(ActorSystem("some name",
    ConfigFactory.parseString("""{ 
        testing-mailbox = {
            mailbox-type = "my.package.TestingMailbox" 
        }
    }"""))) 
    with BeforeAndAfterAll // ... and so on
设置此选项后,我可以如下更改我的测试:

actorUnderTest = system.actorOf(Props[MyActor]).withMailbox("testing-mailbox"))
myActor ! Message("A")
val nextMessage = TestingMailbox.nextMessage(actorUnderTest)
promiseFromApiCall.success(Message("B"))
Await.ready(nextMessage, 3.seconds)
myActor ! Message("C")
myActor ! Message("A")
receiveMessageAfter { promiseFromApiCall.success(Message("B")) }
myActor ! Message("C")
使用一个小助手方法,我甚至可以这样写:

actorUnderTest = system.actorOf(Props[MyActor]).withMailbox("testing-mailbox"))
myActor ! Message("A")
val nextMessage = TestingMailbox.nextMessage(actorUnderTest)
promiseFromApiCall.success(Message("B"))
Await.ready(nextMessage, 3.seconds)
myActor ! Message("C")
myActor ! Message("A")
receiveMessageAfter { promiseFromApiCall.success(Message("B")) }
myActor ! Message("C")
这是我的自定义邮箱:

import akka.actor.{ActorRef, ActorSystem}
import akka.dispatch._
import com.typesafe.config.Config 
import scala.concurrent.{Future, Promise}

object TestingMailbox {

  val promisesByReceiver =
    scala.collection.concurrent.TrieMap[ActorRef, Promise[Any]]()

  class MessageQueue extends UnboundedMailbox.MessageQueue {

    override def enqueue(receiver: ActorRef, handle: Envelope): Unit = {
      super.enqueue(receiver, handle)
      promisesByReceiver.remove(receiver).foreach(_.success(handle.message))
    }

  }

  def nextMessage(receiver: ActorRef): Future[Any] =
    promisesByReceiver.getOrElseUpdate(receiver, Promise[Any]).future

}

class TestingMailbox extends MailboxType
  with ProducesMessageQueue[TestingMailbox.MessageQueue] {

  import TestingMailbox._

  def this(settings: ActorSystem.Settings, config: Config) = this()

  final override def create(owner: Option[ActorRef],
                            system: Option[ActorSystem]) =
      new MessageQueue()

}

对不起,我的问题可能有误导性。这并不是要使参与者在不同的消息顺序方面更加健壮,而是要在测试中生成特定的顺序,以确保它能够处理它。为了说明这一点,我在问题后面附加了另一段。对于有状态的actor,您是否可以使用相同的方法将stash用作所有调用到被测actor的转发累积代理?因此,只需将所有消息发送给该代理参与者,他的角色是确保消息B仅在消息A等之后转发。@DanilaPolevshikov问题是,当代理在将来完成时向自己发送消息时,参与者将不知道代理。@jeffrey chung我终于找到了一个解决方案,并将其作为答案发布。谢谢你的帮助。