5

アクターをテストしているときに、アクターが予期せず (バグが原因で) 例外をスローするという状況が発生し、何度か噛まれましたが、テストは引き続きパスします。ほとんどの場合、アクターの例外は、テストが検証しているものは何でも適切に出力されないため、テストが失敗することを意味しますが、まれに、そうではない場合があります。例外はテスト ランナーとは別のスレッドで発生するため、テスト ランナーはそれについて何も知りません。

1 つの例として、依存関係が呼び出されることを確認するためにモックを使用しているときに、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」は成功しますが、下流の「okActor」は何らかの理由で失敗します。よくわかりません...次の例外があります。

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 を調べましたが、おそらく何か役立つものがあるのではないかと疑っていますが、それを見ることができません。私が見つけた唯一のドキュメントは、予期しない例外ではなく、予期される例外をチェックするためにそれを使用することに関するものでした。

4

3 に答える 3

4

さて、私はこれで遊ぶ時間が少しありました。イベントリスナーとフィルターを使用してエラーをキャッチする優れたソリューションがあります。(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次のように、特定の場所でインラインを使用するか、スイートに混ぜて、すべてwithFixtureのテストでグローバルに使用することができます。

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

これを元の例で使用すると、最初のテスト「メッセージを受信したときにのみ foo を呼び出す」が失敗することになります。ただし、システムが爆発するため、ダウンストリーム テストも失敗します。これを修正するために、さらに一歩進んで、 を使用してテストごとfixture.Suiteに個別にインスタンス化しました。TestKitこれにより、ノイズの多いアクターがいる場合に発生する可能性のある他の多くのテスト分離の問題が解決されます。各テストを宣言する式がもう少し必要ですが、それだけの価値があると思います。この特性を元の例で使用すると、最初のテストが失敗し、2 番目のテストが成功します。

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()
    }
  }
}
于 2013-09-18T19:02:51.763 に答える
4

ログを調べる以外に、アクターがクラッシュしたときにテストを失敗させる 2 つの方法を考えることができます。

  • Terminated メッセージが受信されていないことを確認する
  • TestActorRef.isTerminated プロパティを確認してください

後者のオプションは推奨されていないため、無視します。

プローブから他のアクターを監視する では、 TestProbeをセットアップする方法について説明しています。この場合、次のようになります。

val probe = TestProbe()
probe watch ref

// Actual test goes here ...

probe.expectNoMessage()

例外が原因でアクターが死亡した場合、Terminated メッセージが生成されます。テスト中にそれが発生し、何か別のことが予想される場合、テストは失敗します。最後のメッセージの期待の後に発生した場合、Terminated を受信したときに expectNoMessage() は失敗するはずです。

于 2013-09-12T02:30:01.973 に答える