【问题标题】:Failing a scalatest when akka actor throws exception outside of the test thread当 akka 演员在测试线程之外抛出异常时,scalatest 失败
【发布时间】:2014-02-20 05:20:51
【问题描述】:

我遇到过几次情况,我正在测试一个 Actor 并且 Actor 意外抛出异常(由于错误),但测试仍然通过。现在大多数情况下,Actor 中的异常意味着无论测试正在验证什么都不会正确输出,因此测试失败,但在极少数情况下并非如此。异常发生在与测试运行程序不同的线程中,因此测试运行程序对此一无所知。

一个例子是当我使用模拟来验证某些依赖项是否被调用时,由于 Actor 代码中的错误,我在模拟中调用了一个意外的方法。这会导致模拟抛出异常,这会炸毁演员但不会炸毁测试。有时这甚至会导致 下游 测试由于 Actor 的爆炸而神秘地失败。例如:

// using scala 2.10, akka 2.1.1, scalatest 1.9.1, easymock 3.1
// (FunSpec and TestKit)
class SomeAPI {
  def foo(x: String) = println(x)
  def bar(y: String) = println(y)
}

class SomeActor(someApi: SomeAPI) extends Actor {
  def receive = {
    case x:String  =>
      someApi.foo(x)
      someApi.bar(x)
  }
}

describe("problem example") {
  it("calls foo only when it receives a message") {
    val mockAPI = mock[SomeAPI]
    val ref = TestActorRef(new SomeActor(mockAPI))

    expecting {
      mockAPI.foo("Hi").once()
    }

    whenExecuting(mockAPI) {
      ref.tell("Hi", testActor)
    }
  }

  it("ok actor") {
    val ref = TestActorRef(new Actor {
      def receive = {
        case "Hi"  => sender ! "Hello"
      }
    })
    ref.tell("Hi", testActor)
    expectMsg("Hello")
  }
}

“problemExample”通过了,但是下游的“ok actor”由于某种我不太明白的原因失败了……除了这个例外:

cannot reserve actor name '$$b': already terminated
java.lang.IllegalStateException: cannot reserve actor name '$$b': already terminated
at       akka.actor.dungeon.ChildrenContainer$TerminatedChildrenContainer$.reserve(ChildrenContainer.scala:86)
at akka.actor.dungeon.Children$class.reserveChild(Children.scala:78)
at akka.actor.ActorCell.reserveChild(ActorCell.scala:306)
at akka.testkit.TestActorRef.<init>(TestActorRef.scala:29)

因此,我可以通过检查 afterEach 处理程序中的记录器输出来了解捕获此类事情的方法。绝对可行,尽管在我实际期望异常的情况下有点复杂,这就是我要测试的。但是有没有更直接的方法来处理这个并使测试失败?

附录:我查看了 TestEventListener 并怀疑那里可能有一些有用的东西,但我看不到它。我能找到的唯一文档是关于使用它来检查预期的异常,而不是意外的异常。

【问题讨论】:

    标签: scala akka scalatest


    【解决方案1】:

    在 Actors 中思考还有另一种解决方案:失败传递给主管,因此这是捕获它们并将它们输入测试过程的理想场所:

    val failures = TestProbe()
    val props = ... // description for the actor under test
    val failureParent = system.actorOf(Props(new Actor {
      val child = context.actorOf(props, "child")
      override val supervisorStrategy = OneForOneStrategy() {
        case f => failures.ref ! f; Stop // or whichever directive is appropriate
      }
      def receive = {
        case msg => child forward msg
      }
    }))
    

    您可以通过发送到failureParent 来发送给被测参与者,所有失败(无论是否预期)都转到failures 探针进行检查。

    【讨论】:

    • 工作得很好,在我只验证异常行为的情况下,使用 testActor(使用 ImplicitSender)而不是 TestProbe 实例。
    • 酷!绝对看起来更“演员”。有机会我会试试的。
    【解决方案2】:

    除了检查日志之外,我还可以想到两种在 actor 崩溃时使测试失败的方法:

    • 确保没有收到已终止的消息
    • 检查 TestActorRef.isTerminated 属性

    后一个选项已弃用,因此我将忽略它。

    Watching Other Actors from Probes 描述了如何设置TestProbe。在这种情况下,它可能看起来像:

    val probe = TestProbe()
    probe watch ref
    
    // Actual test goes here ...
    
    probe.expectNoMessage()
    

    如果参与者因异常而死亡,它将生成 Terminated 消息。如果在测试期间发生这种情况并且您期望其他情况,则测试将失败。如果它发生在您的最后一条消息期望之后,那么当收到 Terminated 时,expectNoMessage() 应该会失败。

    【讨论】:

    • 感谢您的建议。我真的想要一些可以折叠成可重用测试套件特征的东西。当我有机会时,我会考虑如何将其中一个想法付诸实践。
    【解决方案3】:

    好的,我有一点时间玩这个。我有一个很好的解决方案,它使用事件侦听器和过滤器来捕获错误。 (检查 isTerminated 或使用 TestProbes 在更集中的情况下可能很好,但在尝试将某些东西混入任何旧测试时似乎很尴尬。)

    import akka.actor.{Props, Actor, ActorSystem}
    import akka.event.Logging.Error
    import akka.testkit._
    import com.typesafe.config.Config
    import org.scalatest._
    import org.scalatest.matchers.ShouldMatchers
    import org.scalatest.mock.EasyMockSugar
    import scala.collection.mutable
    
    trait AkkaErrorChecking extends ShouldMatchers {
      val system:ActorSystem
      val errors:mutable.MutableList[Error] = new mutable.MutableList[Error]
      val errorCaptureFilter = EventFilter.custom {
        case e: Error =>
          errors += e
          false // don't actually filter out this event - it's nice to see the full output in console.
      }
    
      lazy val testListener = system.actorOf(Props(new akka.testkit.TestEventListener {
        addFilter(errorCaptureFilter)
      }))
    
      def withErrorChecking[T](block: => T) = {
        try {
          system.eventStream.subscribe(testListener, classOf[Error])
          filterEvents(errorCaptureFilter)(block)(system)
          withClue(errors.mkString("Akka error(s):\n", "\n", ""))(errors should be('empty))
        } finally {
          system.eventStream.unsubscribe(testListener)
          errors.clear()
        }
      }
    }
    

    您可以只在特定位置使用withErrorChecking inline,或者将其混合到一个套件中并使用withFixture 在所有测试中全局执行,如下所示:

    trait AkkaErrorCheckingSuite extends AkkaErrorChecking with FunSpec {
      override protected def withFixture(test: NoArgTest) {
        withErrorChecking(test())
      }
    }
    

    如果您在我的原始示例中使用它,那么您将让第一个测试“仅在收到消息时调用 foo”失败,这很好,因为这是真正的失败所在。但是由于系统崩溃,下游测试仍然会失败。为了解决这个问题,我更进一步,使用fixture.Suite 为每个测试实例化一个单独的TestKit。当您有嘈杂的演员时,这解决了许多其他潜在的测试隔离问题。它需要更多的仪式来宣布每个测试,但我认为这是非常值得的。在我原来的例子中使用这个特性,我得到第一个测试失败,第二个通过,这正是我想要的!

    trait IsolatedTestKit extends ShouldMatchers { this: fixture.Suite =>
      type FixtureParam = TestKit
      // override this if you want to pass a Config to the actor system instead of using default reference configuration
      val actorSystemConfig: Option[Config] = None
    
      private val systemNameRegex = "[^a-zA-Z0-9]".r
    
      override protected def withFixture(test: OneArgTest) {
        val fixtureSystem = actorSystemConfig.map(config => ActorSystem(systemNameRegex.replaceAllIn(test.name, "-"), config))
                                             .getOrElse    (ActorSystem (systemNameRegex.replaceAllIn(test.name, "-")))
        try {
          val errorCheck = new AkkaErrorChecking {
            val system = fixtureSystem
          }
          errorCheck.withErrorChecking {
            test(new TestKit(fixtureSystem))
          }
        }
        finally {
          fixtureSystem.shutdown()
        }
      }
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2014-03-09
      • 2014-04-30
      • 1970-01-01
      • 2015-07-24
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多