在将期货与参与者消息混合时,确保测试中的消息顺序

时间:2018-07-09 08:26:39

标签: scala testing akka actor akka-testkit

我正在测试一个使用基于未来异步API的演员。当未来完成时,参与者使用管道模式向自身发送消息:

import akka.pattern.pipe
// ...

// somewhere in the actor's receive method
futureBasedApi.doSomething().pipeTo(self)

在测试中,我模拟了API,因此我通过promises控制将来的完成。但是,这与直接发送给actor的其他消息交错:

myActor ! Message("A")
promiseFromApiCall.success(Message("B"))
myActor ! Message("C")

现在我想知道如何才能保证演员收到并 在测试中处理消息A和C之间的消息B,因为消息B实际上是在另一个线程中发送的,所以我无法控制顺序 演员邮箱在其中接收消息的地方。

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

    每个消息后
  • 睡眠几毫秒,使另一个消息 订购可能性很小

  • 等待演员确认每条消息,尽管 确认仅用于测试

  • 直接将消息B发送给演员,以模拟完成 将来并编写单独的测试,以确保管道样式 正确使用(如果参与者愿意,上面的测试不会失败 不能将结果消息通过管道传递给自己)

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

说明:问题不是如何处理生产中可能以随机顺序接收消息的事实。在测试中控制顺序对于确保角色可以实际处理不同的消息顺序至关重要。

3 个答案:

答案 0 :(得分:1)

一个想法是在您的actor中定义一个标志,以指示该actor是否已收到消息B。当actor收到消息C时,acter可以stash如果该标志为false,则取消隐藏该消息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)
}

答案 1 :(得分:1)

在阅读了更多有关akka的内容之后,我终于找到了一个更好的解决方案:用我可以在测试中观察到的邮箱替换actor邮箱。这样,我可以等到演员完成诺言后,直到演员收到新消息。然后才发送下一条消息。 TestingMailbox的代码在帖子末尾给出。

更新:在Akka Typed中,可以使用BehaviorInterceptor非常优雅地实现此目的。只需用自定义拦截器包装受测的Behavior,该拦截器就转发所有消息和信号,但让您观察它们。 下面列出了无类型Akka的邮箱解决方案。


演员可以这样配置:

actorUnderTest = system.actorOf(Props[MyActor]).withMailbox("testing-mailbox"))

我必须通过提供配置来确保参与者系统知道“ testing-mailbox”:

class MyTest extends TestKit(ActorSystem("some name",
    ConfigFactory.parseString("""{ 
        testing-mailbox = {
            mailbox-type = "my.package.TestingMailbox" 
        }
    }"""))) 
    with BeforeAndAfterAll // ... and so on

设置好之后,我可以像这样更改测试:

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()

}

答案 2 :(得分:0)

如果订购消息非常重要,则应使用onBindViewHolder()ask)返回?并将它们链接起来,即使您不希望演员有任何回应。