当akka actor在测试线程之外抛出异常时,没有使用scalatest

ryr*_*guy 5 scala akka scalatest

我有一个情况出现并咬我几次我正在测试一个Actor并且Actor意外地抛出一个异常(由于一个bug),但测试仍然通过.现在大多数情况下,Actor中的异常意味着无论测试验证是否都不正确,因此测试失败,但在极少数情况下这不是真的.异常发生在与测试运行器不同的线程中,因此测试运行器对此一无所知.

一个例子是当我使用mock来验证一些依赖项被调用时,由于Actor代码中的错误,我在mock中调用了一个意外的方法.这导致模拟抛出一个异常,炸毁了演员而不是测试.有时这甚至可能导致下游测试因为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")
  }
}
Run Code Online (Sandbox Code Playgroud)

"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)
Run Code Online (Sandbox Code Playgroud)

所以,我可以通过检查afterEach处理程序中的记录器输出来看到捕获此类事物的方法.绝对可行,虽然在我实际期待异常的情况下有点复杂,而这正是我想要测试的.但有没有更直接的方法处理这个并使测试失败?

附录:我看过TestEventListener并怀疑那里可能会有什么帮助,但我看不到它.我能找到的唯一文件是用它来检查预期的异常,而不是意外的异常.

Rol*_*uhn 9

在演员中思考还有另一种解决方案:故障传递给主管,因此这是捕捉它们并将它们输入测试程序的理想场所:

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
  }
}))
Run Code Online (Sandbox Code Playgroud)

你可以发送给被测试的演员,发送到failureParent所有的故障 - 预期或不是 - 去failures探测器进行检查.


小智 5

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

  • 确保未收到终止消息
  • 检查 TestActorRef.isTermminate 属性

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

从探针中观看其他参与者描述了如何设置TestProbe。在这种情况下,它可能看起来像:

val probe = TestProbe()
probe watch ref

// Actual test goes here ...

probe.expectNoMessage()
Run Code Online (Sandbox Code Playgroud)

如果 actor 由于异常而死亡,它将生成 Termerated 消息。如果在测试期间发生这种情况并且您期望发生其他情况,则测试将失败。如果它发生在您最后一条消息期望之后,则当收到 Termulated 时,expectNoMessage() 应该失败。


ryr*_*guy 5

好吧,我花了一点时间玩这个。我有一个很好的解决方案,它使用事件侦听器和过滤器来捕获错误。(检查 isTermminate 或使用 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()
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

您可以仅withErrorChecking在特定位置使用内联,或者将其混合到套件中并用于withFixture在所有测试中全局执行此操作,如下所示:

trait AkkaErrorCheckingSuite extends AkkaErrorChecking with FunSpec {
  override protected def withFixture(test: NoArgTest) {
    withErrorChecking(test())
  }
}
Run Code Online (Sandbox Code Playgroud)

如果您在我原来的示例中使用它,那么您将得到第一个测试“仅在收到消息时调用 foo”失败,这很好,因为这就是真正的失败所在。但由于系统崩溃,下游测试仍然会失败。为了解决这个问题,我更进一步,使用 a为每个测试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()
    }
  }
}
Run Code Online (Sandbox Code Playgroud)