当Akka actor在测试线程之外引发异常时,失败的原因是scalatest [英] Failing a scalatest when akka actor throws exception outside of the test thread

查看:108
本文介绍了当Akka actor在测试线程之外引发异常时,失败的原因是scalatest的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我遇到了一种情况,在我测试一个Actor时咬了我几次,Actor意外地抛出了一个异常(由于一个错误),但是测试仍然通过了。现在,大多数情况下,Actor中的异常意味着测试所验证的内容均无法正确显示,因此测试失败,但是在极少数情况下,情况并非如此。异常发生在与测试运行程序不同的线程中,因此测试运行程序对此一无所知。

I've had a situation come up and bite me a few times where I'm testing an Actor and the Actor throws an exception unexpectedly (due to a bug), but the test still passes. Now most of the time the exception in the Actor means that whatever the test is verifying won't come out properly so it the test fails, but in rare cases that's not true. The exception occurs in a different thread than the test runner so the test runner knows nothing about it.

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

One example is when I'm using a mock to verify some dependency gets called, and due to a mistake in the Actor code I call an unexpected method in the mock. That causes the mock to throw an exception which blows up the actor but not the test. Sometimes this can even cause downstream tests to fail mysteriously because of how the Actor blew up. For example:

// 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由于某种原因失败我真的不明白...例外:

"problemExample" passes, but then downstream "ok actor" fails for some reason I don't really understand... with this exception:

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处理程序中的记录器输出来了解捕获此类事件的方法。绝对可行,尽管在我确实希望出现异常的情况下有点复杂,而这正是我要测试的情况。但是,还有其他更直接的方法来处理此问题并使测试失败吗?

So, I can see ways of catching this sort of thing by examining the logger output in afterEach handlers. Definitely doable, although a little complicated in cases where I actually expect an exception and that's what I'm trying to test. But is there any more direct way of handling this and making the test fail?

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

Addendum: I have looked at the TestEventListener and suspect there's maybe something there that would help, but I can't see it. The only documentation I could find was about using it to check for expected exceptions, not unexpected ones.

推荐答案

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

Okay, I've had a little time to play with this. I've got a nice solution that uses an event listener and filter to catch errors. (Checking isTerminated or using TestProbes is probably good in more focused cases but seems awkward when trying to make something to mix into any old test.)

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 内联,或将其混合到Suite中并使用 withFixture 可以在所有测试中全局执行此操作,例如:

You can just use withErrorChecking inline at specific spots, or mix it into a Suite and use withFixture to do it globally across all tests, like this:

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

如果在我的原始示例中使用此方法,则将获得第一个测试仅在以下情况下调用foo它会收到一条消息失败,这很好,因为这是真正的失败所在。但是由于系统崩溃,下游测试也仍然会失败。为了解决这个问题,我更进一步,使用了 fixture.Suite 为每个测试实例化一个单独的 TestKit 。当有嘈杂的参与者时,这解决了许多其他潜在的测试隔离问题。需要进行更多的宣布每个测试的仪式,但我认为这是值得的。在我的原始示例中使用此特征,我得到了第一个测试失败,而第二个测试却通过了,这正是我想要的!

If you use this in my original example, then you will get the first test "calls foo only when it receives a message" to fail, which is nice because that's where the real failure is. But the downstream test will still fail as well due to the system blowing up. To fix that, I went a step further and used a fixture.Suite to instance a separate TestKit for each test. That solves lots of other potential test isolation issues when you have noisy actors. It requires a little more ceremony declaring each test but I think it's well worth it. Using this trait with my original example I get the first test failing and the second one passing which is just what I want!

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

这篇关于当Akka actor在测试线程之外引发异常时,失败的原因是scalatest的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
相关文章
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆