我有一种情况出现并咬我几次我正在测试一个Actor并且Actor意外地抛出一个异常(由于一个bug),但测试仍然通过。现在大多数情况下,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并怀疑那里可能会有什么帮助,但我看不到它。我能找到的唯一文件是用它来检查预期的异常,而不是意外的异常。
答案 0 :(得分: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
}
}))
您可以通过发送至failureParent
发送给受测试的演员,并将所有失败 - 预期或不发送 - 转至failures
探测器进行检查。
答案 1 :(得分:4)
除了检查日志之外,我还可以想到两种方法在演员崩溃时失败测试:
后一个选项已弃用,所以我会忽略它。
Watching Other Actors from Probes介绍了如何设置TestProbe。在这种情况下,它可能看起来像:
val probe = TestProbe()
probe watch ref
// Actual test goes here ...
probe.expectNoMessage()
如果演员因异常而死亡,它将生成Terminated消息。如果在测试期间发生这种情况并且您期望其他内容,则测试将失败。如果它发生在您的上一个消息期望之后,那么expectNoMessage()应该在收到Terminated时失败。
答案 2 :(得分:4)
好的,我有一点时间玩这个。我有一个很好的解决方案,它使用事件监听器和过滤器来捕获错误。 (在更有针对性的情况下,检查被终止或使用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
内联,或将其混合到套件中,并使用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()
}
}
}